99bf3ca7a8
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>
97 lines
2.9 KiB
Go
97 lines
2.9 KiB
Go
package report
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// InfraBackup is the payload pushed to the Hub for disaster recovery.
|
|
type InfraBackup struct {
|
|
CustomerID string `json:"customer_id"`
|
|
Domain string `json:"domain"`
|
|
ControllerVersion string `json:"controller_version"`
|
|
Timestamp string `json:"timestamp"`
|
|
|
|
ControllerConfigB64 string `json:"controller_config_b64"`
|
|
SettingsJSONB64 string `json:"settings_json_b64,omitempty"`
|
|
|
|
DiskLayout backup.DiskLayout `json:"disk_layout"`
|
|
DeployedStacks []InfraStack `json:"deployed_stacks"`
|
|
|
|
ResticPassword string `json:"restic_password,omitempty"`
|
|
CrossDrivePassword string `json:"cross_drive_password,omitempty"`
|
|
}
|
|
|
|
// InfraStack identifies a deployed app for disaster recovery.
|
|
type InfraStack struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
HDDPath string `json:"hdd_path,omitempty"`
|
|
NeedsHDD bool `json:"needs_hdd"`
|
|
}
|
|
|
|
// BuildInfraBackup collects all infrastructure state for Hub backup.
|
|
func BuildInfraBackup(
|
|
customerID, domain, version string,
|
|
controllerYAMLPath string,
|
|
settingsPath string,
|
|
resticPasswordFile string,
|
|
systemDataPath string,
|
|
sett *settings.Settings,
|
|
stackProvider backup.StackDataProvider,
|
|
logger *log.Logger,
|
|
) (*InfraBackup, error) {
|
|
ib := &InfraBackup{
|
|
CustomerID: customerID,
|
|
Domain: domain,
|
|
ControllerVersion: version,
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
// Read and encode controller.yaml (critical — fail if unreadable)
|
|
data, err := os.ReadFile(controllerYAMLPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading controller config %s: %w", controllerYAMLPath, err)
|
|
}
|
|
ib.ControllerConfigB64 = base64.StdEncoding.EncodeToString(data)
|
|
|
|
// Read and encode settings.json (important but non-fatal)
|
|
if data, err := os.ReadFile(settingsPath); err == nil {
|
|
ib.SettingsJSONB64 = base64.StdEncoding.EncodeToString(data)
|
|
} else if !os.IsNotExist(err) {
|
|
logger.Printf("[WARN] Infra backup: could not read settings.json: %v", err)
|
|
}
|
|
|
|
// Read primary restic password (important but non-fatal)
|
|
if data, err := os.ReadFile(resticPasswordFile); err == nil {
|
|
ib.ResticPassword = base64.StdEncoding.EncodeToString(data)
|
|
} else if !os.IsNotExist(err) {
|
|
logger.Printf("[WARN] Infra backup: could not read restic password file: %v", err)
|
|
}
|
|
|
|
// Collect disk layout from fstab + blkid
|
|
ib.DiskLayout = collectDiskLayout(systemDataPath)
|
|
|
|
// Collect deployed stacks
|
|
deployed := stackProvider.ListDeployedStacks()
|
|
for _, s := range deployed {
|
|
ib.DeployedStacks = append(ib.DeployedStacks, InfraStack{
|
|
Name: s.Name,
|
|
DisplayName: s.DisplayName,
|
|
HDDPath: stackProvider.GetStackHDDPath(s.Name),
|
|
NeedsHDD: s.NeedsHDD,
|
|
})
|
|
}
|
|
if ib.DeployedStacks == nil {
|
|
ib.DeployedStacks = []InfraStack{}
|
|
}
|
|
|
|
return ib, nil
|
|
}
|