Files
felhom-agent/internal/pbs/report.go
T
admin 766500dfc3 v0.6.0: slice 6 Phase B — PBS offsite tier (verify + PBS-API client + reporting)
Spike-proven that backup/restore-to-PBS reuse Phase A unchanged; the only new code is
the verify capability, a small PBS-API client, and PBSSnapshot reporting.

- internal/pbs: fingerprint-pinned, token-authed PBS-API client (Verify/Snapshots/
  TaskStatus, node-from-UPID; secret read from /etc/pve/priv/storage/<id>.pw at runtime,
  never logged) + the verify maintenance loop (own cadence, default 6h, NOT gated/journaled,
  like the watchdog) + SnapshotStore.
- hub: PBSSnapshot filled (namespace/type/id/time/size/owner/protected/encrypted/
  verify_state/verify_upid); PBSReporter collector seam; cross-repo golden + bidirectional
  key-set tests; hub handler parses pbs_snapshots + logs a failed-verify WARN.
- backup: report the ACTUAL vzdump mode (parsed from the task log; PVE may downgrade
  snapshot->stop). proxmox.Storage.Username. config PBSVerifyCadence/secret-dir.
  --selftest=pbs-verify. Backup/restore-to-PBS unchanged.

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

85 lines
2.3 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
}
// 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
}