7f11cfb36c
Makes PBS DR storage visible like the restic pool box (v0.64.0), differentiated. Scoping
correction: restic = subaccounts on the shared Hetzner Storage Box (Hetzner API); PBS DR =
the felhom-offsite PBS datastore on the ep0 endpoint VM (NO Hetzner API). Option A
(Viktor-ruled): a read-only `usage` op on the felhom-tenantsync ep0 forced command (twin of
fingerprint), polled by a new hub checker on the 15-min throttle. READ-ONLY throughout.
Phase-0 (gate PASSED): on ep0 (PBS 4.2.3), df -B1 --output=size,used,avail <datastore path>
yields bytes (39990112256/7627939840/... ~19%), read-only, existing sudo context, no admin token.
- scripts/felhom-tenantsync.sh -> v1.2.0: read-only `usage` short-circuit (df on the datastore
path), no customer_id, no admin token, NO mutation. + a bash harness proving zero mutation.
- tenantsync.Client.Usage() + BoxUsage; unknown-op -> typed ErrUsageUnsupported (graceful).
- monitor.PBSDRBoxChecker: OffsiteBoxChecker clone over a usageReader seam; 15-min throttle,
cached PBSBoxSnapshot, escalation-only pbsdr_box_fill on the "pbsdr-box" scope (operator only,
no SaveEvent), recovery re-arm. Fill only. THREE states: ok / unavailable (ep0 <=v1.1.0,
neutral no-alert) / degraded (exec failed, keep last).
- config: Alerting.PBSDRBoxFill{Warn,Crit}Percent (80/90); built with the tenantsync client,
60s sweep, SetPBSDRBox. Hub deploy INDEPENDENT of the ep0 update (graceful degradation).
- web: /offsite splits into Restic + PBS DR hash tabs (endpoint cards under PBS DR); PBS panel;
the single dashboard tile becomes two gauges (RESTIC pct.ratio, PBS DR pct / n/a).
- runbook offsite-endpoint.md 10: v1.2.0 update steps (no sudoers/authorized_keys change).
Tests: 10 Go + the harness; 3 red-proofs (usage mutation, escalation-only, unavailable-drives-band)
confirmed red then restored. go build/vet/test + bash -n + hub confirm gate all pass.
61 lines
2.0 KiB
Go
61 lines
2.0 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/monitor"
|
|
)
|
|
|
|
// PBS DR panel — configured + ok: renders the datastore, capacity/used, fill bar.
|
|
func TestPBSDRPanel_OK(t *testing.T) {
|
|
s, _ := newRenderServer(t)
|
|
s.SetPBSDRBox(func() (monitor.PBSBoxSnapshot, bool) {
|
|
return monitor.PBSBoxSnapshot{
|
|
CapacityBytes: 40 * gib, UsedBytes: 8 * gib, FillPercent: 20, FillBand: "ok",
|
|
State: monitor.PBSStateOK, FetchedAt: time.Now().UTC(),
|
|
}, true
|
|
})
|
|
body := renderOffsite(t, s)
|
|
for _, want := range []string{"PBS DR datastore", "felhom-offsite", "40.0 GB", "8.0 GB", "20% full"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("PBS DR panel missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// PBS DR panel — unavailable (endpoint script ≤ v1.1.0): the honest pending-update message, NOT a fake 0%.
|
|
func TestPBSDRPanel_Unavailable(t *testing.T) {
|
|
s, _ := newRenderServer(t)
|
|
s.SetPBSDRBox(func() (monitor.PBSBoxSnapshot, bool) {
|
|
return monitor.PBSBoxSnapshot{State: monitor.PBSStateUnavailable, FetchedAt: time.Now().UTC()}, true
|
|
})
|
|
body := renderOffsite(t, s)
|
|
if !strings.Contains(body, "usage not available") || !strings.Contains(body, "v1.2.0") {
|
|
t.Fatalf("unavailable PBS panel must show the pending-update message:\n%s", pbsSection(body))
|
|
}
|
|
// Never a fake fill for the unavailable box.
|
|
if strings.Contains(pbsSection(body), "% full") {
|
|
t.Fatal("unavailable PBS panel must not render a fill percentage")
|
|
}
|
|
}
|
|
|
|
// PBS DR panel — not configured (no tenantsync client).
|
|
func TestPBSDRPanel_NotConfigured(t *testing.T) {
|
|
s, _ := newRenderServer(t)
|
|
// s.pbsdrBox left nil
|
|
body := renderOffsite(t, s)
|
|
if !strings.Contains(body, "PBS DR metrics not configured") {
|
|
t.Fatalf("unconfigured PBS panel must say 'not configured'")
|
|
}
|
|
}
|
|
|
|
// pbsSection returns the slice of the body from the PBS DR datastore heading onward (for scoped asserts).
|
|
func pbsSection(body string) string {
|
|
if i := strings.Index(body, "PBS DR datastore"); i >= 0 {
|
|
return body[i:]
|
|
}
|
|
return body
|
|
}
|