80 lines
3.9 KiB
Go
80 lines
3.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// R-1192 follow-up (night 2026-10-21). The guest swap fix (agent: every guest may swap as much as its RAM) moves a
|
|
// memory burst onto the HOST's swap. Our installer gives an 8 GB machine 7.75 GB of it (measured on demo-hp); a host
|
|
// with none — a BYO install, or swap turned off — gets nothing from the fix, and the household's first big upload can
|
|
// freeze the box again. The agent reports both numbers; the hub tells the OPERATOR once per host when the host's swap
|
|
// is below its guests' allowance (host_swap_short), and once when it is covered again (host_swap_ok). Edge-triggered
|
|
// from the host's own newest swap event, so a hub restart does not mail again. Operator-only (notify.operatorOnlyEvents):
|
|
// the household cannot add swap to its host.
|
|
const (
|
|
eventHostSwapShort = "host_swap_short"
|
|
eventHostSwapOK = "host_swap_ok"
|
|
)
|
|
|
|
// installerMaxSwapMB is the most host swap Proxmox's installer gives any machine (Install.pm compute_swapsize: the
|
|
// RAM, at least 4096 MB on a disk of 64 GB or more, at most 8192 MB). Read from the 9.2-1 ISO on 2026-10-22.
|
|
const installerMaxSwapMB = 8192
|
|
|
|
// hostSwapShort is the R-1216 rule (D28 = A, operator 2026-10-22): a host is short of swap when it has none, or when
|
|
// its swap is below 90 % of the smaller of its guests' swap allowance and what the installer gives. Because the guests
|
|
// fit in the host's RAM, „what the installer gives a machine of that RAM" is never smaller than the guests' allowance
|
|
// below 8 GB — so the host's RAM is not needed. The morning's first rule (host swap < the guests' whole allowance)
|
|
// flagged every machine with a guest above 8 GB; the 8 GB soak's burst used 2.7 GB.
|
|
// The CONTRACT is testdata/host_swap_rule_cases.json, byte-identical in felhom-controller (the install page's sentence)
|
|
// — felhom.eu scripts/host_swap_rule_gate.py; TestR1216_HostSwapRuleCases runs it here.
|
|
func hostSwapShort(hostMB, guestMB int64) bool {
|
|
if hostMB <= 0 {
|
|
return true
|
|
}
|
|
need := guestMB
|
|
if need > installerMaxSwapMB {
|
|
need = installerMaxSwapMB
|
|
}
|
|
if need <= 0 {
|
|
return false
|
|
}
|
|
return hostMB*10 < need*9
|
|
}
|
|
|
|
func (h *Handler) checkHostSwap(hostID, custID string, hostMB, guestMB *int64) {
|
|
if hostMB == nil || guestMB == nil || custID == "" {
|
|
return // an older agent: not evaluated
|
|
}
|
|
short := hostSwapShort(*hostMB, *guestMB)
|
|
prev, err := h.store.LatestHostSwapEvent(custID, hostID)
|
|
if err != nil {
|
|
h.logger.Printf("[WARN] host %s swap check: reading the last swap event failed: %v", hostID, err)
|
|
return
|
|
}
|
|
wasShort := prev != nil && prev.EventType == eventHostSwapShort
|
|
var typ, sev, msg string
|
|
switch {
|
|
case short && !wasShort:
|
|
typ, sev = eventHostSwapShort, "warning"
|
|
msg = fmt.Sprintf("Host %s: its swap (%d MB) is too small for its guests' swap allowance (%d MB) — below what our installer "+
|
|
"would give (the guests' allowance, at most 8192 MB). The guest swap fix needs swap on the host behind it; without it a "+
|
|
"memory burst — a big first upload on an 8 GB box — can freeze the box for minutes (R-1192). Add swap on the host or lower "+
|
|
"the guest's RAM. The household cannot fix this.", hostID, *hostMB, *guestMB)
|
|
case !short && wasShort:
|
|
typ, sev = eventHostSwapOK, "info"
|
|
msg = fmt.Sprintf("Host %s: its swap (%d MB) is enough for its guests' swap allowance (%d MB) again.", hostID, *hostMB, *guestMB)
|
|
default:
|
|
return
|
|
}
|
|
h.logger.Printf("[WARN] host %s: %s (host swap %d MB, guests' swap %d MB)", hostID, typ, *hostMB, *guestMB)
|
|
details, _ := json.Marshal(map[string]any{"host_id": hostID, "host_swap_mb": *hostMB, "guest_swap_mb": *guestMB})
|
|
if _, eerr := h.store.SaveEvent(custID, typ, sev, msg, string(details), "hub"); eerr != nil {
|
|
h.logger.Printf("[WARN] %s event save FAILED for %s: %v", typ, hostID, eerr)
|
|
return // the audit row first: a mail without its event row lies
|
|
}
|
|
if h.dispatcher != nil {
|
|
go h.dispatcher.ProcessEvent(custID, typ, sev, msg, string(details), "hub")
|
|
}
|
|
}
|