feat: comprehensive debug logging across all controller modules
Add detailed [DEBUG] logging to every controller module when logging.level is set to "debug". Each module with stateful debug uses SetDebug(bool) wired from main.go. Covers stacks, backup, cloudflare, integrations, system, monitor, settings, scheduler, web handlers, storage, metrics, API, selfupdate, and assets. Also includes the app export/import (.fab bundles) feature from v0.32.0 and its debug page integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ func DiscoverAppData(provider StackDataProvider, discoveredDBs []DiscoveredDB) [
|
||||
info.HasHDDData = len(info.HDDPaths) > 0
|
||||
|
||||
// Discover Docker named volumes from compose
|
||||
info.DockerVolumes = parseComposeNamedVolumes(stack.ComposePath)
|
||||
info.DockerVolumes = ParseComposeNamedVolumes(stack.ComposePath)
|
||||
|
||||
// Check if app has a DB container (already backed up via DB dump)
|
||||
for _, db := range discoveredDBs {
|
||||
@@ -108,8 +108,8 @@ func DiscoverAppData(provider StackDataProvider, discoveredDBs []DiscoveredDB) [
|
||||
return result
|
||||
}
|
||||
|
||||
// parseComposeNamedVolumes extracts named Docker volumes from a docker-compose.yml.
|
||||
func parseComposeNamedVolumes(composePath string) []AppDockerVolume {
|
||||
// ParseComposeNamedVolumes extracts named Docker volumes from a docker-compose.yml.
|
||||
func ParseComposeNamedVolumes(composePath string) []AppDockerVolume {
|
||||
data, err := os.ReadFile(composePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
|
||||
@@ -156,9 +156,11 @@ func NewManager(cfg *config.Config, pinger *monitor.Pinger, sett *settings.Setti
|
||||
if dataDir == "" {
|
||||
dataDir = "/opt/docker/felhom-controller/data"
|
||||
}
|
||||
restic := NewResticManager(cfg, logger)
|
||||
restic.SetDebug(cfg.Logging.Level == "debug")
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
restic: NewResticManager(cfg, logger),
|
||||
restic: restic,
|
||||
logger: logger,
|
||||
pinger: pinger,
|
||||
settings: sett,
|
||||
|
||||
@@ -23,6 +23,7 @@ type ResticManager struct {
|
||||
logger *log.Logger
|
||||
customerID string
|
||||
cacheDir string
|
||||
debug bool
|
||||
}
|
||||
|
||||
// SnapshotResult holds the outcome of a restic backup.
|
||||
@@ -63,9 +64,17 @@ func NewResticManager(cfg *config.Config, logger *log.Logger) *ResticManager {
|
||||
}
|
||||
}
|
||||
|
||||
// SetDebug enables or disables debug logging.
|
||||
func (r *ResticManager) SetDebug(debug bool) {
|
||||
r.debug = debug
|
||||
}
|
||||
|
||||
// EnsureInitialized checks if the restic repo exists and initializes it if not.
|
||||
// Also auto-generates the password file if missing.
|
||||
func (r *ResticManager) EnsureInitialized(repoPath string) error {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] EnsureInitialized: repoPath=%s, passwordFile=%s", repoPath, r.passwordFile)
|
||||
}
|
||||
// Ensure password file exists
|
||||
if _, err := os.Stat(r.passwordFile); os.IsNotExist(err) {
|
||||
if err := r.generatePassword(); err != nil {
|
||||
@@ -109,6 +118,9 @@ func (r *ResticManager) Snapshot(repoPath string, paths []string, tags []string)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Snapshot: repo=%s, paths=%v, tags=%v", repoPath, paths, tags)
|
||||
}
|
||||
|
||||
args := []string{"backup", "--json"}
|
||||
for _, tag := range tags {
|
||||
@@ -129,6 +141,9 @@ func (r *ResticManager) Snapshot(repoPath string, paths []string, tags []string)
|
||||
if len(existingPaths) == 0 {
|
||||
return nil, fmt.Errorf("no backup paths exist")
|
||||
}
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Snapshot: %d/%d paths exist, backing up: %v", len(existingPaths), len(paths), existingPaths)
|
||||
}
|
||||
args = append(args, existingPaths...)
|
||||
|
||||
cmd := r.command(ctx, repoPath, args...)
|
||||
@@ -187,6 +202,11 @@ func (r *ResticManager) Snapshot(repoPath string, paths []string, tags []string)
|
||||
}
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Snapshot: completed in %s, snapshotID=%s, filesNew=%d, filesChanged=%d, dataAdded=%s",
|
||||
result.Duration, result.SnapshotID, result.FilesNew, result.FilesChanged, result.DataAdded)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -195,6 +215,12 @@ func (r *ResticManager) Prune(repoPath string, retention config.RetentionConfig)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Prune: repo=%s, keepDaily=%d, keepWeekly=%d, keepMonthly=%d",
|
||||
repoPath, retention.KeepDaily, retention.KeepWeekly, retention.KeepMonthly)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
args := []string{
|
||||
"forget",
|
||||
"--keep-daily", fmt.Sprintf("%d", retention.KeepDaily),
|
||||
@@ -209,6 +235,9 @@ func (r *ResticManager) Prune(repoPath string, retention config.RetentionConfig)
|
||||
return fmt.Errorf("restic forget/prune failed: %v — %s", err, truncate(string(out), 200))
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Prune: completed in %s, output=%d bytes", time.Since(start), len(out))
|
||||
}
|
||||
r.logger.Printf("[INFO] Restic prune completed for %s", repoPath)
|
||||
return nil
|
||||
}
|
||||
@@ -218,11 +247,23 @@ func (r *ResticManager) Check(repoPath string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Check: repo=%s", repoPath)
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
cmd := r.command(ctx, repoPath, "check")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Check: failed after %s, output=%s", time.Since(start), truncate(string(out), 300))
|
||||
}
|
||||
return fmt.Errorf("restic check failed: %v — %s", err, truncate(string(out), 200))
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Check: repo=%s OK, completed in %s", repoPath, time.Since(start))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -231,6 +272,10 @@ func (r *ResticManager) ListSnapshots(repoPath string, limit int) ([]SnapshotInf
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] ListSnapshots: repo=%s, limit=%d", repoPath, limit)
|
||||
}
|
||||
|
||||
cmd := r.command(ctx, repoPath, "snapshots", "--json")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -251,6 +296,11 @@ func (r *ResticManager) ListSnapshots(repoPath string, limit int) ([]SnapshotInf
|
||||
snapshots = snapshots[:limit]
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] ListSnapshots: repo=%s, found %d total snapshots, returning %d",
|
||||
repoPath, len(snapshots), len(snapshots))
|
||||
}
|
||||
|
||||
return snapshots, nil
|
||||
}
|
||||
|
||||
@@ -259,6 +309,10 @@ func (r *ResticManager) LatestSnapshot(repoPath string) (*SnapshotInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] LatestSnapshot: repo=%s", repoPath)
|
||||
}
|
||||
|
||||
cmd := r.command(ctx, repoPath, "snapshots", "--latest", "1", "--json")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -271,9 +325,17 @@ func (r *ResticManager) LatestSnapshot(repoPath string) (*SnapshotInfo, error) {
|
||||
}
|
||||
|
||||
if len(snapshots) == 0 {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] LatestSnapshot: repo=%s, no snapshots found", repoPath)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] LatestSnapshot: repo=%s, id=%s, time=%s, paths=%v",
|
||||
repoPath, snapshots[0].ID, snapshots[0].Time.Format(time.RFC3339), snapshots[0].Paths)
|
||||
}
|
||||
|
||||
return &snapshots[0], nil
|
||||
}
|
||||
|
||||
@@ -282,6 +344,11 @@ func (r *ResticManager) Stats(repoPath string) (*RepoStats, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] Stats: repo=%s", repoPath)
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
stats := &RepoStats{}
|
||||
|
||||
// Get repo size
|
||||
@@ -311,6 +378,15 @@ func (r *ResticManager) Stats(repoPath string) (*RepoStats, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
latestID := "none"
|
||||
if stats.LatestSnapshot != nil {
|
||||
latestID = stats.LatestSnapshot.ID
|
||||
}
|
||||
r.logger.Printf("[DEBUG] [restic] Stats: repo=%s, totalSize=%s, snapshots=%d, latest=%s, took %s",
|
||||
repoPath, stats.TotalSize, stats.SnapshotCount, latestID, time.Since(start))
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -336,6 +412,12 @@ func (r *ResticManager) RestoreAppData(repoPath string, snapshotID string, paths
|
||||
args = append(args, "--include", p)
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] RestoreAppData: repo=%s, snapshot=%s, %d include paths=%v",
|
||||
repoPath, snapshotID, len(paths), paths)
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
r.logger.Printf("[WARN] RESTORE started: repo=%s, snapshot=%s, paths=%v", repoPath, snapshotID, paths)
|
||||
|
||||
cmd := r.command(ctx, repoPath, args...)
|
||||
@@ -345,14 +427,22 @@ func (r *ResticManager) RestoreAppData(repoPath string, snapshotID string, paths
|
||||
return fmt.Errorf("restic restore failed: %w", err)
|
||||
}
|
||||
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] RestoreAppData: completed in %s, output=%d bytes", time.Since(start), len(output))
|
||||
}
|
||||
r.logger.Printf("[INFO] RESTORE completed: snapshot=%s, paths=%v", snapshotID, paths)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RepoExists checks if a restic repo is initialized at the given path.
|
||||
func (r *ResticManager) RepoExists(repoPath string) bool {
|
||||
exists := false
|
||||
_, err := os.Stat(filepath.Join(repoPath, "config"))
|
||||
return err == nil
|
||||
exists = err == nil
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] RepoExists: repo=%s, exists=%v", repoPath, exists)
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
// UnlockCommand returns an exec.Cmd that runs restic unlock on the given repo.
|
||||
@@ -361,6 +451,9 @@ func (r *ResticManager) UnlockCommand(ctx context.Context, repoPath string) *exe
|
||||
}
|
||||
|
||||
func (r *ResticManager) command(ctx context.Context, repoPath string, args ...string) *exec.Cmd {
|
||||
if r.debug {
|
||||
r.logger.Printf("[DEBUG] [restic] command: restic %s (repo=%s)", strings.Join(args, " "), repoPath)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "restic", args...)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"RESTIC_REPOSITORY="+repoPath,
|
||||
|
||||
@@ -10,19 +10,26 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RestoreAppFromBackup restores a single app from its cross-drive backup.
|
||||
// Steps: restore config → verify/restore data → copy DB dumps → docker compose up.
|
||||
func RestoreAppFromBackup(ctx context.Context, app *RestorableApp, stacksDir string, logger *log.Logger) error {
|
||||
stackDir := filepath.Join(stacksDir, app.Name)
|
||||
start := time.Now()
|
||||
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: app=%s, stackDir=%s, hasConfig=%v, hasData=%v, hasDBDump=%v, hasRsyncData=%v",
|
||||
app.Name, stackDir, app.HasConfig, app.HasData, app.HasDBDump, app.HasRsyncData)
|
||||
|
||||
// Step 1: Restore stack config from _config/ backup
|
||||
if app.HasConfig {
|
||||
logger.Printf("[INFO] Restoring config for %s from %s", app.Name, app.ConfigPath)
|
||||
stepStart := time.Now()
|
||||
if err := restoreConfigDir(ctx, app.ConfigPath, stackDir); err != nil {
|
||||
return fmt.Errorf("restoring config: %w", err)
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: config restore for %s completed in %s", app.Name, time.Since(stepStart))
|
||||
} else {
|
||||
// No config backup — check if stack dir already exists (from catalog sync)
|
||||
if !dirExists(stackDir) {
|
||||
@@ -35,20 +42,29 @@ func RestoreAppFromBackup(ctx context.Context, app *RestorableApp, stacksDir str
|
||||
if app.NeedsHDD && !app.HasData && app.HasRsyncData {
|
||||
// App data is missing but rsync backup exists — restore it
|
||||
logger.Printf("[INFO] Restoring user data for %s from rsync backup", app.Name)
|
||||
stepStart := time.Now()
|
||||
if err := restoreUserData(ctx, app, logger); err != nil {
|
||||
logger.Printf("[WARN] User data restore failed for %s: %v", app.Name, err)
|
||||
// Non-fatal: app might still start without all data
|
||||
} else {
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: user data restore for %s completed in %s", app.Name, time.Since(stepStart))
|
||||
}
|
||||
} else if app.HasData {
|
||||
logger.Printf("[INFO] App data for %s found at %s — no restore needed", app.Name, app.DataPath)
|
||||
} else {
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: %s — no user data to restore (needsHDD=%v, hasData=%v, hasRsyncData=%v)",
|
||||
app.Name, app.NeedsHDD, app.HasData, app.HasRsyncData)
|
||||
}
|
||||
|
||||
// Step 3: Copy DB dumps to primary backup location
|
||||
if app.HasDBDump {
|
||||
logger.Printf("[INFO] Restoring DB dumps for %s", app.Name)
|
||||
stepStart := time.Now()
|
||||
if err := restoreDBDumps(app, logger); err != nil {
|
||||
logger.Printf("[WARN] DB dump restore failed for %s: %v", app.Name, err)
|
||||
// Non-fatal
|
||||
} else {
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: DB dump restore for %s completed in %s", app.Name, time.Since(stepStart))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,22 +78,30 @@ func RestoreAppFromBackup(ctx context.Context, app *RestorableApp, stacksDir str
|
||||
}
|
||||
|
||||
composeDir := filepath.Dir(composePath)
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: %s using compose file %s", app.Name, composePath)
|
||||
|
||||
logger.Printf("[INFO] Pulling images for %s", app.Name)
|
||||
pullStart := time.Now()
|
||||
pullCmd := exec.CommandContext(ctx, "docker", "compose", "-f", composePath, "pull")
|
||||
pullCmd.Dir = composeDir
|
||||
if out, err := pullCmd.CombinedOutput(); err != nil {
|
||||
logger.Printf("[WARN] docker compose pull failed for %s: %v (%s)", app.Name, err, strings.TrimSpace(string(out)))
|
||||
// Non-fatal: might work with cached images
|
||||
} else {
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: docker compose pull for %s completed in %s", app.Name, time.Since(pullStart))
|
||||
}
|
||||
|
||||
logger.Printf("[INFO] Starting %s", app.Name)
|
||||
upStart := time.Now()
|
||||
upCmd := exec.CommandContext(ctx, "docker", "compose", "-f", composePath, "up", "-d")
|
||||
upCmd.Dir = composeDir
|
||||
if out, err := upCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("docker compose up: %v (%s)", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: %s fully restored and started in %s", app.Name, time.Since(start))
|
||||
logger.Printf("[DEBUG] [backup] RestoreAppFromBackup: docker compose up for %s completed in %s", app.Name, time.Since(upStart))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -103,6 +127,8 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
return fmt.Errorf("no rsync data path or HDD path")
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: app=%s, rsyncPath=%s, hddPath=%s", app.Name, app.RsyncDataPath, app.HDDPath)
|
||||
|
||||
// The rsync backup contains the app's data directories.
|
||||
// Walk the backup dir and rsync each subdirectory (excluding _config/_db)
|
||||
// back to the app's HDD data directory.
|
||||
@@ -112,10 +138,12 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
}
|
||||
|
||||
dataDir := AppDataDir(app.HDDPath, app.Name)
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — target dataDir=%s, %d entries in backup", app.Name, dataDir, len(entries))
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating data dir: %w", err)
|
||||
}
|
||||
|
||||
restored := 0
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if name == "_config" || name == "_db" || strings.HasPrefix(name, ".") {
|
||||
@@ -132,9 +160,12 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
continue
|
||||
}
|
||||
dst = strings.TrimRight(dst, "/") + "/"
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — rsync dir %s → %s", app.Name, src, dst)
|
||||
cmd := exec.CommandContext(ctx, "rsync", "-a", src, dst)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
logger.Printf("[WARN] rsync data %s: %v (%s)", name, err, strings.TrimSpace(string(out)))
|
||||
} else {
|
||||
restored++
|
||||
}
|
||||
} else {
|
||||
// Single file — copy directly
|
||||
@@ -143,12 +174,16 @@ func restoreUserData(ctx context.Context, app *RestorableApp, logger *log.Logger
|
||||
logger.Printf("[WARN] Cannot read %s: %v", src, err)
|
||||
continue
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — copying file %s (%d bytes)", app.Name, name, len(data))
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
logger.Printf("[WARN] Cannot write %s: %v", dst, err)
|
||||
} else {
|
||||
restored++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] restoreUserData: %s — restored %d items", app.Name, restored)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -170,6 +205,7 @@ func restoreDBDumps(app *RestorableApp, logger *log.Logger) error {
|
||||
}
|
||||
|
||||
destDir := AppDBDumpPath(drivePath, app.Name)
|
||||
logger.Printf("[DEBUG] [backup] restoreDBDumps: app=%s, src=%s, destDir=%s", app.Name, app.DBDumpPath, destDir)
|
||||
if err := os.MkdirAll(destDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating dump dir: %w", err)
|
||||
}
|
||||
@@ -179,6 +215,7 @@ func restoreDBDumps(app *RestorableApp, logger *log.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
copied := 0
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
@@ -190,11 +227,15 @@ func restoreDBDumps(app *RestorableApp, logger *log.Logger) error {
|
||||
logger.Printf("[WARN] Cannot read dump %s: %v", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] restoreDBDumps: %s — copying %s (%d bytes)", app.Name, e.Name(), len(data))
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
logger.Printf("[WARN] Cannot write dump %s: %v", e.Name(), err)
|
||||
} else {
|
||||
copied++
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] restoreDBDumps: %s — copied %d dump files", app.Name, copied)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -25,22 +25,38 @@ import (
|
||||
// Returns the list of successfully mounted final mount paths.
|
||||
func MountDrivesFromLayout(ctx context.Context, layout DiskLayout, logger *log.Logger) ([]string, error) {
|
||||
if len(layout.Mounts) == 0 {
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: no mounts in layout, nothing to do")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: processing %d mount entries from disk layout", len(layout.Mounts))
|
||||
|
||||
// Get current block devices with UUIDs
|
||||
uuidToDevice, err := scanBlockDeviceUUIDs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning block devices: %w", err)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: discovered %d block devices with UUIDs", len(uuidToDevice))
|
||||
for uuid, dev := range uuidToDevice {
|
||||
uuidShort := uuid
|
||||
if len(uuidShort) > 12 {
|
||||
uuidShort = uuidShort[:12]
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: device %s → UUID=%s...", dev, uuidShort)
|
||||
}
|
||||
|
||||
var mounted []string
|
||||
|
||||
for _, dm := range layout.Mounts {
|
||||
if dm.UUID == "" {
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: skipping mount entry with empty UUID (label=%s)", dm.Label)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: processing %s (UUID=%s, mountPoint=%s, rawMount=%s, fsType=%s)",
|
||||
dm.Label, dm.UUID, dm.MountPoint, dm.RawMount, dm.FSType)
|
||||
|
||||
// Find matching device by UUID
|
||||
device := uuidToDevice[dm.UUID]
|
||||
if device == "" {
|
||||
@@ -72,12 +88,15 @@ func MountDrivesFromLayout(ctx context.Context, layout DiskLayout, logger *log.L
|
||||
// Mount using the appropriate pattern
|
||||
if dm.RawMount != "" && dm.BindSubdir != "" {
|
||||
// Two-layer HDD mount: raw → bind
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: %s — two-layer mount (raw=%s, bindSubdir=%s)",
|
||||
dm.Label, dm.RawMount, dm.BindSubdir)
|
||||
if err := mountRawAndBind(ctx, device, dm, logger); err != nil {
|
||||
logger.Printf("[ERROR] Failed to mount %s: %v", dm.Label, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Simple direct mount (e.g., sys_drive)
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: %s — direct mount to %s", dm.Label, dm.MountPoint)
|
||||
if err := mountDirect(ctx, device, dm, logger); err != nil {
|
||||
logger.Printf("[ERROR] Failed to mount %s: %v", dm.Label, err)
|
||||
continue
|
||||
@@ -93,10 +112,11 @@ func MountDrivesFromLayout(ctx context.Context, layout DiskLayout, logger *log.L
|
||||
logger.Printf("[INFO] Successfully mounted %s at %s", dm.Label, finalMount)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] MountDrivesFromLayout: done — %d/%d drives mounted", len(mounted), len(layout.Mounts))
|
||||
return mounted, nil
|
||||
}
|
||||
|
||||
// scanBlockDeviceUUIDs runs lsblk + blkid to build a UUID → device path map.
|
||||
// scanBlockDeviceUUIDs runs lsblk + blkid to build a UUID -> device path map.
|
||||
func scanBlockDeviceUUIDs(ctx context.Context) (map[string]string, error) {
|
||||
// First try lsblk with UUID output
|
||||
out, err := exec.CommandContext(ctx, "lsblk", "-J", "-o", "NAME,UUID,FSTYPE,MOUNTPOINT").Output()
|
||||
@@ -172,10 +192,12 @@ func mountDirect(ctx context.Context, device string, dm DiskMount, logger *log.L
|
||||
|
||||
// Use host device path if available
|
||||
devPath := hostDevPath(device)
|
||||
logger.Printf("[DEBUG] [backup] mountDirect: mount -t %s -o noatime %s %s", dm.FSType, devPath, dm.MountPoint)
|
||||
cmd := exec.CommandContext(ctx, "mount", "-t", dm.FSType, "-o", "noatime", devPath, dm.MountPoint)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("mount %s: %s: %w", devPath, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] mountDirect: %s mounted successfully at %s", devPath, dm.MountPoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,12 +209,14 @@ func mountRawAndBind(ctx context.Context, device string, dm DiskMount, logger *l
|
||||
}
|
||||
|
||||
devPath := hostDevPath(device)
|
||||
logger.Printf("[DEBUG] [backup] mountRawAndBind: layer 1 — mount -t %s -o noatime %s %s", dm.FSType, devPath, dm.RawMount)
|
||||
cmd := exec.CommandContext(ctx, "mount", "-t", dm.FSType, "-o", "noatime", devPath, dm.RawMount)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("raw mount %s → %s: %s: %w", devPath, dm.RawMount, strings.TrimSpace(string(out)), err)
|
||||
return fmt.Errorf("raw mount %s -> %s: %s: %w", devPath, dm.RawMount, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] mountRawAndBind: layer 1 OK — %s mounted at %s", devPath, dm.RawMount)
|
||||
|
||||
// Layer 2: bind mount (subdir → final mount point)
|
||||
// Layer 2: bind mount (subdir -> final mount point)
|
||||
bindSrc := filepath.Join(dm.RawMount, dm.BindSubdir)
|
||||
if err := os.MkdirAll(bindSrc, 0755); err != nil {
|
||||
return fmt.Errorf("creating bind source dir: %w", err)
|
||||
@@ -201,10 +225,12 @@ func mountRawAndBind(ctx context.Context, device string, dm DiskMount, logger *l
|
||||
return fmt.Errorf("creating final mount point: %w", err)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] mountRawAndBind: layer 2 — mount --bind %s %s", bindSrc, dm.MountPoint)
|
||||
cmd = exec.CommandContext(ctx, "mount", "--bind", bindSrc, dm.MountPoint)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("bind mount %s → %s: %s: %w", bindSrc, dm.MountPoint, strings.TrimSpace(string(out)), err)
|
||||
return fmt.Errorf("bind mount %s -> %s: %s: %w", bindSrc, dm.MountPoint, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
logger.Printf("[DEBUG] [backup] mountRawAndBind: layer 2 OK — %s bound to %s", bindSrc, dm.MountPoint)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -213,6 +239,8 @@ func mountRawAndBind(ctx context.Context, device string, dm DiskMount, logger *l
|
||||
func addDRFstabEntries(dm DiskMount, logger *log.Logger) error {
|
||||
const fstabPath = "/host-fstab"
|
||||
|
||||
logger.Printf("[DEBUG] [backup] addDRFstabEntries: checking fstab for %s (UUID=%s)", dm.Label, dm.UUID)
|
||||
|
||||
data, err := os.ReadFile(fstabPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading fstab: %w", err)
|
||||
@@ -222,6 +250,7 @@ func addDRFstabEntries(dm DiskMount, logger *log.Logger) error {
|
||||
|
||||
// Skip if UUID already in fstab (idempotent)
|
||||
if strings.Contains(content, dm.UUID) {
|
||||
logger.Printf("[DEBUG] [backup] addDRFstabEntries: UUID %s already in fstab — skipping", dm.UUID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,9 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: scanning %d mount paths, %d stacks from manifest",
|
||||
len(mountedPaths), len(stacks))
|
||||
|
||||
// Build drive info and find backup directories
|
||||
type driveBackup struct {
|
||||
drivePath string
|
||||
@@ -181,6 +184,8 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
Available: avail,
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: checking drive %s (label=%s, available=%v)", mp, label, avail)
|
||||
|
||||
secPath := SecondaryBackupPath(mp)
|
||||
if dirExists(secPath) {
|
||||
di.HasBackup = true
|
||||
@@ -195,6 +200,8 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
plan.Drives = append(plan.Drives, di)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: found %d drives with backup data", len(backupDrives))
|
||||
|
||||
// For each stack from the manifest, look for backup data on drives
|
||||
for _, stack := range stacks {
|
||||
app := RestorableApp{
|
||||
@@ -205,12 +212,16 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: scanning for app %s (needsHDD=%v, hddPath=%s)",
|
||||
stack.Name, stack.NeedsHDD, stack.HDDPath)
|
||||
|
||||
// Check if app data exists directly on HDD (common case: HDD survived)
|
||||
if stack.HDDPath != "" {
|
||||
dataDir := AppDataDir(stack.HDDPath, stack.Name)
|
||||
if dirExists(dataDir) {
|
||||
app.HasData = true
|
||||
app.DataPath = dataDir
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: %s — live data found at %s", stack.Name, dataDir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +235,8 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
// Found a backup for this app
|
||||
app.DrivePath = db.drivePath
|
||||
app.DriveLabel = db.label
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: %s — backup found on drive %s at %s",
|
||||
stack.Name, db.label, rsyncBase)
|
||||
|
||||
// Check for _config/ (stack compose directory backup)
|
||||
configDir := filepath.Join(rsyncBase, "_config")
|
||||
@@ -245,6 +258,9 @@ func ScanDrivesForBackups(mountedPaths []string, stacks []InfraStackInfo, logger
|
||||
app.RsyncDataPath = rsyncBase
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] [backup] ScanDrivesForBackups: %s — config=%v, dbDump=%v, rsyncData=%v",
|
||||
stack.Name, app.HasConfig, app.HasDBDump, app.HasRsyncData)
|
||||
|
||||
break // use first drive with backup for this app
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user