Files
admin fbe113011d v0.39.0 — DR-recipe completion: live PBS coord + drop role/restic_repo_coord from v1 drive shape
Live PBS coord: new internal/pbs/live_reporter.go (LiveSnapshotReporter implements
hub.PBSReporter via the cheap Client.Snapshots() list with last-known-good fallback,
bounded by an 8s timeout, list-only — never triggers a verify). Closes the gap where
the recipe's pbs block was omitted whenever the verify-loop SnapshotStore was empty
(one-shot collect + the first ~6h after a daemon restart). SnapshotStore.Get added
(per-datastore LKG). Wired into the collector in both runDaemon and runSelftestHub;
the verify loop keeps Recording into the SAME shared store via one hoisted pbsTargets.

v1 host-half drive shape: dropped drives[].role (hub/operator-owned manifest concept,
not host-derivable) and drives[].restic_repo_coord (named a backup tier that doesn't
exist). Drive shape is now {durable_id, mount_path, intent, fs_type?, total_bytes}.
Hub reads drives as json.RawMessage → no hub struct change; goldens re-pinned
byte-identical (agent + hub copies).

Tests: live_reporter_test.go (T1 load-bearing coord-without-verify + T2..T6),
TestDRRecipeHostHalf_V1DriveShape; each companion demonstrated to fail pre-fix then
reverted. go build/vet/test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:25:09 +02:00

100 lines
2.8 KiB
Go

package pbs
import (
"context"
"sync"
"time"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)
// verify-state constants for the reported PBSSnapshot.
const (
VerifyOK = "ok"
VerifyFailed = "failed"
VerifyNone = "none" // no verify has run yet (PBS omits the verification field)
)
// ToHub maps a PBS API Snapshot to the hub.PBSSnapshot wire record (slice 6 Phase B). The
// backup-time epoch becomes RFC3339; `encrypted` is derived from the data files'
// crypt-mode (any "encrypt" → encrypted); verify_state is "none" until a verify runs.
func (s Snapshot) ToHub() hub.PBSSnapshot {
ns := s.Namespace
if ns == "" {
ns = "root"
}
out := hub.PBSSnapshot{
Namespace: ns,
BackupType: s.BackupType,
BackupID: s.BackupID,
BackupTime: time.Unix(s.BackupTime, 0).UTC().Format(time.RFC3339),
SizeBytes: s.Size,
Owner: s.Owner,
Protected: s.Protected,
Encrypted: s.encrypted(),
VerifyState: VerifyNone,
}
if s.Verification != nil {
out.VerifyState = s.Verification.State
out.VerifyUPID = s.Verification.UPID
}
return out
}
// encrypted reports whether the snapshot's DATA is client-side encrypted (any data file with
// crypt-mode "encrypt"; index.json is "sign-only" and is ignored).
func (s Snapshot) encrypted() bool {
for _, f := range s.Files {
if f.CryptMode == "encrypt" {
return true
}
}
return false
}
// SnapshotStore holds the latest reported PBS snapshots per datastore — the point-in-time
// state the host-report surfaces. The verify loop writes it; the collector reads it via the
// hub PBSReporter seam. Mutex-guarded (concurrent collector vs loop).
type SnapshotStore struct {
mu sync.Mutex
byDatastore map[string][]hub.PBSSnapshot
}
// NewSnapshotStore builds an empty store.
func NewSnapshotStore() *SnapshotStore {
return &SnapshotStore{byDatastore: map[string][]hub.PBSSnapshot{}}
}
// Record replaces the snapshot set for a datastore.
func (s *SnapshotStore) Record(datastore string, snaps []hub.PBSSnapshot) {
s.mu.Lock()
defer s.mu.Unlock()
s.byDatastore[datastore] = snaps
}
// Get returns a copy of the last-known-good snapshot set for ONE datastore (nil when absent or
// empty). It is the per-datastore fallback the live reporter reaches for when a live list fails —
// distinct from PBSSnapshots, which aggregates every datastore.
func (s *SnapshotStore) Get(datastore string) []hub.PBSSnapshot {
s.mu.Lock()
defer s.mu.Unlock()
src := s.byDatastore[datastore]
if len(src) == 0 {
return nil
}
out := make([]hub.PBSSnapshot, len(src))
copy(out, src)
return out
}
// PBSSnapshots implements hub.PBSReporter — all known snapshots across datastores.
func (s *SnapshotStore) PBSSnapshots(context.Context) []hub.PBSSnapshot {
s.mu.Lock()
defer s.mu.Unlock()
out := []hub.PBSSnapshot{}
for _, snaps := range s.byDatastore {
out = append(out, snaps...)
}
return out
}