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:
+44
-5
@@ -31,12 +31,25 @@ type StorageObserver interface {
|
||||
Observe(ctx context.Context) ([]StorageTarget, error)
|
||||
}
|
||||
|
||||
// BackupReporter / RestoreTestReporter are the slice-6 seams the backup layer plugs into
|
||||
// (same consumer-side pattern as StorageObserver — hub does not import the backup package).
|
||||
// They return the agent's LATEST-known backup-per-target / restore-test result (point-in-time
|
||||
// state the backup layer accumulates), not a live scan. A nil reporter → empty slice.
|
||||
type BackupReporter interface {
|
||||
Backups(ctx context.Context) []Backup
|
||||
}
|
||||
type RestoreTestReporter interface {
|
||||
RestoreTests(ctx context.Context) []RestoreTest
|
||||
}
|
||||
|
||||
// Collector builds a HostReport from read-only sources. All deps are behind narrow
|
||||
// interfaces for unit testing.
|
||||
type Collector struct {
|
||||
px proxmoxReader
|
||||
cf CloudflaredProber
|
||||
storage StorageObserver
|
||||
backups BackupReporter
|
||||
restoreTests RestoreTestReporter
|
||||
hostID string
|
||||
agentVersion string
|
||||
logger *slog.Logger
|
||||
@@ -44,8 +57,8 @@ type Collector struct {
|
||||
}
|
||||
|
||||
// NewCollector builds a collector. hostID echoes config.Hub.HostID; agentVersion is
|
||||
// the binary version. storage may be nil (storage_targets emitted empty).
|
||||
func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserver, hostID, agentVersion string, logger *slog.Logger) *Collector {
|
||||
// the binary version. storage/backups/restoreTests may be nil (their collections emit empty).
|
||||
func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserver, backups BackupReporter, restoreTests RestoreTestReporter, hostID, agentVersion string, logger *slog.Logger) *Collector {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
@@ -53,6 +66,8 @@ func NewCollector(px proxmoxReader, cf CloudflaredProber, storage StorageObserve
|
||||
px: px,
|
||||
cf: cf,
|
||||
storage: storage,
|
||||
backups: backups,
|
||||
restoreTests: restoreTests,
|
||||
hostID: hostID,
|
||||
agentVersion: agentVersion,
|
||||
logger: logger,
|
||||
@@ -79,9 +94,10 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
// storage_targets populated this slice (slice 5) via the observer; the rest stay
|
||||
// defined-but-empty (slice 6). Non-nil so they marshal as [].
|
||||
StorageTargets: c.collectStorage(ctx),
|
||||
Backups: []Backup{},
|
||||
RestoreTests: []RestoreTest{},
|
||||
PBSSnapshots: []PBSSnapshot{},
|
||||
Backups: c.collectBackups(ctx),
|
||||
RestoreTests: c.collectRestoreTests(ctx),
|
||||
PBSSnapshots: []PBSSnapshot{}, // Phase B
|
||||
|
||||
AuditTail: []AuditEntry{},
|
||||
Cloudflared: Cloudflared{Status: c.cloudflaredStatus(ctx)},
|
||||
}
|
||||
@@ -159,6 +175,29 @@ func (c *Collector) collectStorage(ctx context.Context) []StorageTarget {
|
||||
return targets
|
||||
}
|
||||
|
||||
// collectBackups / collectRestoreTests read the agent's latest backup + restore-test state
|
||||
// via the seams. Best-effort: a nil reporter or nil slice degrades to an empty (non-nil)
|
||||
// list so the collection always marshals as [].
|
||||
func (c *Collector) collectBackups(ctx context.Context) []Backup {
|
||||
if c.backups == nil {
|
||||
return []Backup{}
|
||||
}
|
||||
if b := c.backups.Backups(ctx); b != nil {
|
||||
return b
|
||||
}
|
||||
return []Backup{}
|
||||
}
|
||||
|
||||
func (c *Collector) collectRestoreTests(ctx context.Context) []RestoreTest {
|
||||
if c.restoreTests == nil {
|
||||
return []RestoreTest{}
|
||||
}
|
||||
if r := c.restoreTests.RestoreTests(ctx); r != nil {
|
||||
return r
|
||||
}
|
||||
return []RestoreTest{}
|
||||
}
|
||||
|
||||
func (c *Collector) cloudflaredStatus(ctx context.Context) string {
|
||||
if c.cf == nil {
|
||||
return "unknown"
|
||||
|
||||
Reference in New Issue
Block a user