v0.6.0-rc1: slice 6 Phase A — backup + the self-restore-test (local target)
The guest-level backup layer + the journaled self-restore-test (restore→boot→verify→ teardown) that closes "a backup you haven't restored isn't a backup". All benign (reuses the slice-4 classifier/gate/journal; no new destructive class/crypto). Local target only; PBS = Phase B. Restore to a NEW guest only. Backups crash-consistent. - proxmox: DestroyLXC, VzdumpOptions.Notes (notes-template), LatestBackupVolID. - reconcile: Engine.RunRestoreTest (journal Scratch entry BEFORE mutation; net link-down pre-boot; defer teardown always; benign gated destroy) + Recover extended to reap a leaked scratch guest (Scratch flag, special-cased before the UPID path; idempotent). - internal/backup: runner (vzdump + archive resolve + bulk-gap = backup!=1) + cadence scheduler (4th daemon goroutine, default 24h) + in-memory report store. - hub: Backup/RestoreTest filled; collector seams; cross-repo golden byte-identical + bidirectional key-set tests; hub handler logs a FAILED restore-test prominently. - config BackupConfig (band 990000-990009 default); --selftest=backup / restore-test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,9 +27,73 @@ type Config struct {
|
||||
Authz AuthzConfig `json:"authz"`
|
||||
Hub HubConfig `json:"hub"`
|
||||
Storage StorageConfig `json:"storage"`
|
||||
Backup BackupConfig `json:"backup"`
|
||||
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
|
||||
}
|
||||
|
||||
// BackupConfig tunes the slice-6 backup + self-restore-test layer. The restore-test runs on
|
||||
// an agent-internal cadence (no hub policy needed — it's self-validation); the backup
|
||||
// schedule/retention/target-selection policy is hub-manifest-owned and unfed until slice 10.
|
||||
type BackupConfig struct {
|
||||
// LocalBackupTarget is the vzdump storage (content=backup) backups go to, e.g. "local".
|
||||
LocalBackupTarget string `json:"local_backup_target"`
|
||||
// RestoreStorage is where a restore-test's restored rootfs lands, e.g. "local-lvm".
|
||||
RestoreStorage string `json:"restore_storage"`
|
||||
// RestoreTestCadenceSeconds is the self-restore-test interval; 0 → default (24h).
|
||||
// Set negative to DISABLE the automatic cadence (on-demand selftest still works).
|
||||
RestoreTestCadenceSeconds int `json:"restore_test_cadence_seconds"`
|
||||
// ScratchVMIDMin/Max bound the throwaway restore-test scratch-guest VMID band. The
|
||||
// restore-test refuses to run unless this is a valid band (min>0, max>=min); 9999 is
|
||||
// always excluded. Defaults to 990000–990009.
|
||||
ScratchVMIDMin int `json:"scratch_vmid_min"`
|
||||
ScratchVMIDMax int `json:"scratch_vmid_max"`
|
||||
}
|
||||
|
||||
// Default scratch VMID band + restore-test cadence.
|
||||
const (
|
||||
defaultScratchVMIDMin = 990000
|
||||
defaultScratchVMIDMax = 990009
|
||||
defaultRestoreTestCadence = 24 * time.Hour
|
||||
)
|
||||
|
||||
// RestoreTestCadence returns the configured restore-test interval: a positive value as-is,
|
||||
// 0 → 24h default, negative → 0 (disabled).
|
||||
func (b BackupConfig) RestoreTestCadence() time.Duration {
|
||||
switch {
|
||||
case b.RestoreTestCadenceSeconds > 0:
|
||||
return time.Duration(b.RestoreTestCadenceSeconds) * time.Second
|
||||
case b.RestoreTestCadenceSeconds < 0:
|
||||
return 0 // disabled
|
||||
default:
|
||||
return defaultRestoreTestCadence
|
||||
}
|
||||
}
|
||||
|
||||
// ScratchBand returns the effective [min,max] scratch VMID band (defaults applied).
|
||||
func (b BackupConfig) ScratchBand() (min, max int) {
|
||||
min, max = b.ScratchVMIDMin, b.ScratchVMIDMax
|
||||
if min == 0 && max == 0 {
|
||||
return defaultScratchVMIDMin, defaultScratchVMIDMax
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
// ValidateForRestoreTest checks the scratch band is usable. Called only when the restore-test
|
||||
// cadence is enabled (so a host that never restore-tests needn't configure a band).
|
||||
func (b BackupConfig) ValidateForRestoreTest() error {
|
||||
min, max := b.ScratchBand()
|
||||
if min <= 0 || max < min {
|
||||
return fmt.Errorf("config: backup.scratch_vmid_[min,max] is an invalid band [%d,%d]", min, max)
|
||||
}
|
||||
if 9999 >= min && 9999 <= max {
|
||||
return fmt.Errorf("config: backup scratch band [%d,%d] must not include the standing scratch 9999", min, max)
|
||||
}
|
||||
if b.RestoreStorage == "" {
|
||||
return fmt.Errorf("config: backup.restore_storage is required when the restore-test cadence is enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StorageConfig tunes the storage watchdog (slice 5). All optional — zero values fall back
|
||||
// to the storage package defaults via the accessor methods. The watchdog poll is FAST
|
||||
// (seconds) to catch a USB drop quickly; the debounce keeps a flapping drive from storming
|
||||
@@ -203,6 +267,14 @@ func applyEnv(cfg *Config) {
|
||||
}
|
||||
cfg.Hub.PollSeconds = envInt("FELHOM_AGENT_HUB_POLL_SECONDS", cfg.Hub.PollSeconds)
|
||||
cfg.Hub.TimeoutSeconds = envInt("FELHOM_AGENT_HUB_TIMEOUT_SECONDS", cfg.Hub.TimeoutSeconds)
|
||||
// backup (slice 6)
|
||||
if v := os.Getenv("FELHOM_AGENT_BACKUP_LOCAL_TARGET"); v != "" {
|
||||
cfg.Backup.LocalBackupTarget = v
|
||||
}
|
||||
if v := os.Getenv("FELHOM_AGENT_BACKUP_RESTORE_STORAGE"); v != "" {
|
||||
cfg.Backup.RestoreStorage = v
|
||||
}
|
||||
cfg.Backup.RestoreTestCadenceSeconds = envInt("FELHOM_AGENT_BACKUP_RESTORE_TEST_CADENCE_SECONDS", cfg.Backup.RestoreTestCadenceSeconds)
|
||||
}
|
||||
|
||||
// envInt overlays an int env var, keeping cur (with a stderr warning) on parse
|
||||
|
||||
Reference in New Issue
Block a user