R-1192 follow-up: report host swap vs the guests' swap allowance (log once, hub alarm, local API); R-1194: no OOB stanza before the first felhom-sshd pass; unreleased
gates / gates (push) Successful in 1m56s
gates / gates (push) Successful in 1m56s
This commit is contained in:
1 parent
286a7e3ecf
commit
0da792e670
10 files changed
+215
-3
No files matched your search
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/capability"
|
||||
@@ -136,6 +137,8 @@ type Collector struct {
|
||||
selfSHA func() string // R-349: sha256 of the running binary; default runningBinarySHA256
|
||||
logger *slog.Logger
|
||||
now func() time.Time
|
||||
// swapShortWarned (R-1192): the host-swap-short WARN is logged once per state change, not every report.
|
||||
swapShortWarned atomic.Bool
|
||||
}
|
||||
|
||||
// NewCollector builds a collector. hostID echoes config.Hub.HostID; agentVersion is
|
||||
@@ -375,6 +378,8 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
}
|
||||
|
||||
host := hostMetrics(c.px.Node(), ns)
|
||||
guests := c.collectGuests(ctx)
|
||||
c.applySwap(&host, ns, guests) // R-1192: host swap vs the guests' swap allowance
|
||||
host.CPUTempC = c.cpuTempC(ctx) // slice 9: operator freebie — temp now rides the hub report too
|
||||
host.WrapperSHA256 = pbsWrapperSHA256() // R-50b(a): make privileged-artifact drift answerable
|
||||
report := &HostReport{
|
||||
@@ -383,7 +388,7 @@ func (c *Collector) Collect(ctx context.Context) (*HostReport, error) {
|
||||
AgentVersion: c.agentVersion,
|
||||
AgentSHA256: c.agentSHA256(),
|
||||
Host: host,
|
||||
Guests: c.collectGuests(ctx),
|
||||
Guests: guests,
|
||||
// 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),
|
||||
@@ -567,6 +572,7 @@ func (c *Collector) collectGuests(ctx context.Context) []Guest {
|
||||
Cores: cfg.Cores,
|
||||
MemoryBytes: g.MaxMem,
|
||||
DiskBytes: g.MaxDisk,
|
||||
SwapBytes: cfg.Swap * (1 << 20),
|
||||
}
|
||||
}
|
||||
guests = append(guests, entry)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
// R-1192 follow-up (night 2026-10-21): the guest swap fix (every guest may swap as much as its RAM,
|
||||
// localapi/guestswap.go) moves a memory burst onto the HOST's swap. Measured on demo-hp the same day: our installer
|
||||
// gives an 8 GB / 256 GB machine 7.75 GB of host swap, so a 4096 MB guest's 4096 MB swap fits. A host with no swap
|
||||
// (a BYO install, or swap turned off) gets nothing from the fix. So the report carries both numbers, the agent says it
|
||||
// once in its log, and the hub raises host_swap_short. The agent never creates host swap itself: changing a host its
|
||||
// owner set up is a decision (felhom.eu register).
|
||||
|
||||
// HostSwapShort reports whether the host's swap is below the guests' swap allowance. A host whose guests have no
|
||||
// swap allowance (0) is never short.
|
||||
func HostSwapShort(hostMB, guestMB int64) bool { return guestMB > 0 && hostMB < guestMB }
|
||||
|
||||
// applySwap fills the host block's swap numbers from the node status and the guests' specs, and logs the short
|
||||
// state once per change.
|
||||
func (c *Collector) applySwap(h *HostMetrics, ns proxmox.NodeStatus, guests []Guest) {
|
||||
hostMB := ns.Swap.Total / (1 << 20)
|
||||
var guestMB int64
|
||||
for _, g := range guests {
|
||||
if g.Spec != nil {
|
||||
guestMB += g.Spec.SwapBytes / (1 << 20)
|
||||
}
|
||||
}
|
||||
h.HostSwapMB, h.GuestSwapMB = &hostMB, &guestMB
|
||||
if HostSwapShort(hostMB, guestMB) {
|
||||
if c.swapShortWarned.CompareAndSwap(false, true) {
|
||||
c.logger.Warn("host-swap: the host's swap is below its guests' swap allowance — a memory burst can freeze the box (R-1192); the hub is told (host_swap_short)",
|
||||
"host_swap_mb", hostMB, "guest_swap_mb", guestMB)
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.swapShortWarned.CompareAndSwap(true, false) {
|
||||
c.logger.Info("host-swap: the host's swap covers its guests' swap allowance again", "host_swap_mb", hostMB, "guest_swap_mb", guestMB)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
// R-1192 follow-up (night 2026-10-21): the host report carries the host's swap and the guests' swap allowance, and the
|
||||
// agent says ONCE in its log when the host's swap is below it (and once when it is covered again).
|
||||
// RED-PROOF: with applySwap not called the numbers are nil; with the CompareAndSwap removed the WARN repeats.
|
||||
func TestR1192_HostReportCarriesSwapAndWarnsOnce(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Collector{logger: slog.New(slog.NewTextHandler(&buf, nil))}
|
||||
guests := []Guest{{VMID: 9201, Spec: &GuestSpec{SwapBytes: 4096 << 20}}, {VMID: 9202, Spec: nil}}
|
||||
var ns proxmox.NodeStatus // a host with no swap
|
||||
for i := 0; i < 3; i++ {
|
||||
h := HostMetrics{}
|
||||
c.applySwap(&h, ns, guests)
|
||||
if h.HostSwapMB == nil || h.GuestSwapMB == nil || *h.HostSwapMB != 0 || *h.GuestSwapMB != 4096 {
|
||||
t.Fatalf("report numbers: host %v guest %v", h.HostSwapMB, h.GuestSwapMB)
|
||||
}
|
||||
}
|
||||
if n := strings.Count(buf.String(), "host-swap: the host's swap is below"); n != 1 {
|
||||
t.Fatalf("the short WARN must be logged once, got %d:\n%s", n, buf.String())
|
||||
}
|
||||
ns.Swap.Total = 7935 << 20 // the installer's 8 GB host
|
||||
h := HostMetrics{}
|
||||
c.applySwap(&h, ns, guests)
|
||||
if *h.HostSwapMB != 7935 || HostSwapShort(*h.HostSwapMB, *h.GuestSwapMB) {
|
||||
t.Fatalf("a covered host must not be short: %v / %v", *h.HostSwapMB, *h.GuestSwapMB)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "covers its guests' swap allowance again") {
|
||||
t.Fatalf("the recovery must be said once:\n%s", buf.String())
|
||||
}
|
||||
if HostSwapShort(0, 0) {
|
||||
t.Fatal("a host whose guests have no swap allowance is never short")
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,11 @@ type HostMetrics struct {
|
||||
//
|
||||
// Empty = unreadable/absent, which the hub treats as UNKNOWN, never as drift.
|
||||
WrapperSHA256 string `json:"wrapper_sha256,omitempty"`
|
||||
// HostSwapMB / GuestSwapMB (R-1192 follow-up, night 2026-10-21): the host's swap (Proxmox node status — the
|
||||
// kernel's SwapTotal) and the sum of its guests' swap allowance. The guest swap fix (swap = RAM) needs swap ON THE
|
||||
// HOST behind it; the hub raises host_swap_short when the first is below the second. nil = not read (older agent).
|
||||
HostSwapMB *int64 `json:"host_swap_mb,omitempty"`
|
||||
GuestSwapMB *int64 `json:"guest_swap_mb,omitempty"`
|
||||
}
|
||||
|
||||
// Guest is one LXC. The agent reports vmid; the hub derives the guest PK
|
||||
@@ -355,6 +360,7 @@ type GuestSpec struct {
|
||||
Cores int `json:"cores"`
|
||||
MemoryBytes int64 `json:"memory_bytes"`
|
||||
DiskBytes int64 `json:"disk_bytes"`
|
||||
SwapBytes int64 `json:"swap_bytes,omitempty"` // R-1192: the guest's swap allowance (PVE config `swap`, MB → bytes)
|
||||
}
|
||||
|
||||
// Cloudflared is the box's tunnel (R-841, agent v0.141.0): the cloudflared container in the customer guest.
|
||||
|
||||
Reference in new issue
Block a user