6713df2186
Complete DR implementation (TASK2.md Phases 1-4): - Hub infra-backup push/pull endpoints (controller.yaml, disk layout, stacks) - Fresh-deployment detection pulls config from Hub, auto-mounts drives by UUID - Full-page restore UI with drive status, app table, sequential restore - docker-setup.sh shows DR instructions when customer_id is configured New files: disk_layout.go, restore_scan.go, restore_app_linux.go, restore_drives_linux.go, infra_backup.go, infra_pull.go, handler_restore.go, restore.html Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
3.3 KiB
Go
136 lines
3.3 KiB
Go
//go:build linux
|
|
|
|
package report
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
)
|
|
|
|
// collectDiskLayout reads /host-fstab and correlates with blkid/lsblk to build
|
|
// the disk mount topology. Only includes data partitions (not root, boot, or swap).
|
|
func collectDiskLayout(systemDataPath string) backup.DiskLayout {
|
|
layout := backup.DiskLayout{}
|
|
|
|
fstabPath := "/host-fstab"
|
|
if _, err := os.Stat(fstabPath); err != nil {
|
|
fstabPath = "/etc/fstab"
|
|
}
|
|
|
|
data, err := os.ReadFile(fstabPath)
|
|
if err != nil {
|
|
return layout
|
|
}
|
|
|
|
// Parse fstab into UUID-based entries and bind mount entries
|
|
type fstabEntry struct {
|
|
source string
|
|
mountPoint string
|
|
fsType string
|
|
options string
|
|
}
|
|
|
|
var uuidEntries []fstabEntry
|
|
var bindEntries []fstabEntry
|
|
|
|
systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true}
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
source := fields[0]
|
|
mountPoint := fields[1]
|
|
fsType := fields[2]
|
|
options := fields[3]
|
|
|
|
// Skip system mounts and swap
|
|
if systemMounts[mountPoint] || fsType == "swap" {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(source, "UUID=") {
|
|
uuidEntries = append(uuidEntries, fstabEntry{
|
|
source: strings.TrimPrefix(source, "UUID="),
|
|
mountPoint: mountPoint,
|
|
fsType: fsType,
|
|
options: options,
|
|
})
|
|
} else if fsType == "none" && strings.Contains(options, "bind") {
|
|
bindEntries = append(bindEntries, fstabEntry{
|
|
source: source,
|
|
mountPoint: mountPoint,
|
|
options: options,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Process UUID-based entries
|
|
for _, e := range uuidEntries {
|
|
dm := backup.DiskMount{
|
|
UUID: e.source,
|
|
MountPoint: e.mountPoint,
|
|
FSType: e.fsType,
|
|
FstabOptions: e.options,
|
|
}
|
|
|
|
// Get label via blkid
|
|
if out, err := exec.Command("blkid", "-o", "value", "-s", "LABEL", "-U", e.source).Output(); err == nil {
|
|
dm.Label = strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// Get size via lsblk (resolve UUID to device first)
|
|
if devPath, err := exec.Command("blkid", "-U", e.source).Output(); err == nil {
|
|
dev := strings.TrimSpace(string(devPath))
|
|
if dev != "" {
|
|
if out, err := exec.Command("lsblk", "-b", "-n", "-o", "SIZE", dev).Output(); err == nil {
|
|
if sz, err := strconv.ParseInt(strings.TrimSpace(string(out)), 10, 64); err == nil {
|
|
dm.SizeBytes = sz
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine role
|
|
if e.mountPoint == systemDataPath {
|
|
dm.Role = "system_data"
|
|
} else {
|
|
dm.Role = "hdd_storage"
|
|
}
|
|
|
|
// Check for a corresponding bind mount
|
|
for _, bind := range bindEntries {
|
|
if strings.HasPrefix(bind.source, e.mountPoint+"/") {
|
|
subdir := strings.TrimPrefix(bind.source, e.mountPoint+"/")
|
|
dm.BindSubdir = subdir
|
|
dm.RawMount = e.mountPoint
|
|
dm.MountPoint = bind.mountPoint // the final user-facing mount point
|
|
break
|
|
}
|
|
}
|
|
|
|
// Get label from mount point basename as fallback
|
|
if dm.Label == "" {
|
|
if dm.RawMount != "" {
|
|
dm.Label = filepath.Base(dm.RawMount)
|
|
} else {
|
|
dm.Label = filepath.Base(dm.MountPoint)
|
|
}
|
|
}
|
|
|
|
layout.Mounts = append(layout.Mounts, dm)
|
|
}
|
|
|
|
return layout
|
|
}
|