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 }