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.
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package web
|
|
|
|
// PBS DR datastore fill surfaces (v0.65.0, R-5): the Offsite "PBS DR" tab panel + the Dashboard PBS
|
|
// gauge. The web layer reads only the checker's cached snapshot (s.pbsdrBox) — it never polls ep0. The
|
|
// snapshot carries its own state (ok / unavailable / degraded) so the UI renders each honestly.
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/monitor"
|
|
)
|
|
|
|
// pbsdrBoxView is the PBS-DR panel model.
|
|
type pbsdrBoxView struct {
|
|
Configured bool // s.pbsdrBox wired (tenantsync client present)
|
|
Pending bool // configured but the first poll hasn't landed yet
|
|
State string // ok | unavailable | degraded
|
|
Unavailable bool // endpoint script predates the usage op (≤ v1.1.0) — expected pre-update
|
|
Degraded bool // last poll failed — values shown stale
|
|
HasFill bool // numbers available (ok or degraded-with-last-known)
|
|
CapacityStr string
|
|
UsedStr string
|
|
FillPercent float64
|
|
FillBand string
|
|
FetchedAt time.Time
|
|
}
|
|
|
|
// pbsdrTile is the compact Dashboard PBS gauge: fill %, band-colored; "n/a" when unavailable.
|
|
type pbsdrTile struct {
|
|
Unavailable bool
|
|
FillPercent float64
|
|
Band string
|
|
Degraded bool
|
|
}
|
|
|
|
// pbsdrBoxData builds the PBS-DR panel view. Nil provider → Configured:false ("not configured"). Never polls.
|
|
func (s *Server) pbsdrBoxData() pbsdrBoxView {
|
|
if s.pbsdrBox == nil {
|
|
return pbsdrBoxView{Configured: false}
|
|
}
|
|
view := pbsdrBoxView{Configured: true}
|
|
snap, ok := s.pbsdrBox()
|
|
if !ok {
|
|
view.Pending = true // configured, first poll pending
|
|
return view
|
|
}
|
|
view.State = snap.State
|
|
view.FetchedAt = snap.FetchedAt
|
|
switch snap.State {
|
|
case monitor.PBSStateUnavailable:
|
|
view.Unavailable = true
|
|
case monitor.PBSStateDegraded:
|
|
view.Degraded = true
|
|
if snap.CapacityBytes > 0 { // last-known values persist across a degraded poll
|
|
view.HasFill = true
|
|
view.CapacityStr = fmtBytesGB(snap.CapacityBytes)
|
|
view.UsedStr = fmtBytesGB(snap.UsedBytes)
|
|
view.FillPercent = snap.FillPercent
|
|
view.FillBand = snap.FillBand
|
|
}
|
|
default: // ok
|
|
view.HasFill = true
|
|
view.CapacityStr = fmtBytesGB(snap.CapacityBytes)
|
|
view.UsedStr = fmtBytesGB(snap.UsedBytes)
|
|
view.FillPercent = snap.FillPercent
|
|
view.FillBand = snap.FillBand
|
|
}
|
|
return view
|
|
}
|
|
|
|
// pbsdrBoxTile builds the Dashboard PBS gauge, or nil when there is nothing to show (no provider, or the
|
|
// first poll hasn't landed). Unavailable → a neutral "n/a" gauge (never a fake 0%).
|
|
func (s *Server) pbsdrBoxTile() *pbsdrTile {
|
|
if s.pbsdrBox == nil {
|
|
return nil
|
|
}
|
|
snap, ok := s.pbsdrBox()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if snap.State == monitor.PBSStateUnavailable {
|
|
return &pbsdrTile{Unavailable: true}
|
|
}
|
|
return &pbsdrTile{
|
|
FillPercent: snap.FillPercent,
|
|
Band: snap.FillBand,
|
|
Degraded: snap.State == monitor.PBSStateDegraded,
|
|
}
|
|
}
|