hub v0.132.0: the System page (versions + OS updates with the ring/switch/approve buttons, R-852), the Hosts Proxmox/kernel column, the operator-approved Docker engine release (2 healthy ring-0 nights), the crash-guard events (R-851); evidence audits/os-docker-crash-2026-10-04
gates / gates (push) Successful in 30s
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// Package sysfacts reads the agent's `system` stanza (agent v0.142.0, R-852, `09` decision 89): the box's Proxmox,
|
||||
// kernel, Debian and Docker versions and its crash guard. One parser for the System page, the Hosts page and the
|
||||
// crash events. A value the box could not read stays "unknown" — never empty, never guessed. Pinned by sysfacts_test.go.
|
||||
package sysfacts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Unknown is what a field reads when nobody could read it.
|
||||
const Unknown = "unknown"
|
||||
|
||||
// CrashGuard is the box's felhom-crash-guard state (`11` §5.9).
|
||||
type CrashGuard struct {
|
||||
Armed bool `json:"armed"`
|
||||
Tripped bool `json:"tripped"`
|
||||
TrippedAt string `json:"tripped_at"`
|
||||
TrippedReason string `json:"tripped_reason"`
|
||||
UncleanBoots []string `json:"unclean_boots"`
|
||||
InWindow int `json:"unclean_boots_in_window"`
|
||||
In24h int `json:"unclean_boots_24h"`
|
||||
LastBootAt string `json:"last_boot_at"`
|
||||
LastBootUnclean bool `json:"last_boot_unclean"`
|
||||
KernelPanic *int `json:"kernel_panic"`
|
||||
BootID string `json:"boot_id"`
|
||||
RearmedAt string `json:"rearmed_at"`
|
||||
RearmedBy string `json:"rearmed_by"`
|
||||
}
|
||||
|
||||
// Host is the Proxmox host's half.
|
||||
type Host struct {
|
||||
Debian string `json:"debian"`
|
||||
KernelRunning string `json:"kernel_running"`
|
||||
KernelNextBoot string `json:"kernel_next_boot"`
|
||||
KernelNextBootSource string `json:"kernel_next_boot_source"`
|
||||
Held []string `json:"held"` // nil = unknown
|
||||
Tainted *int `json:"tainted"`
|
||||
OopsThisBoot *bool `json:"oops_this_boot"`
|
||||
KernelPanic *int `json:"kernel_panic"`
|
||||
CrashGuard *CrashGuard `json:"crash_guard"`
|
||||
}
|
||||
|
||||
// Guest is the customer guest's half.
|
||||
type Guest struct {
|
||||
Debian string `json:"debian"`
|
||||
DockerEngine string `json:"docker_engine"`
|
||||
Containerd string `json:"containerd"`
|
||||
LiveRestore string `json:"live_restore"` // on | off | unknown
|
||||
UnknownReason string `json:"unknown_reason"`
|
||||
}
|
||||
|
||||
// System is the whole stanza. Present is false for a report from an agent older than v0.142.0.
|
||||
type System struct {
|
||||
Present bool
|
||||
PVEVersion string
|
||||
KernelVersion string
|
||||
VMID int
|
||||
FactsError string
|
||||
ReadAt string
|
||||
Host Host
|
||||
Guest Guest
|
||||
}
|
||||
|
||||
type wire struct {
|
||||
System *struct {
|
||||
PVEVersion string `json:"pve_version"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
VMID int `json:"vmid"`
|
||||
Facts json.RawMessage `json:"facts"`
|
||||
FactsError string `json:"facts_error"`
|
||||
ReadAt string `json:"read_at"`
|
||||
} `json:"system"`
|
||||
}
|
||||
|
||||
func orUnknown(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return Unknown
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Parse reads the stanza from a host-report body. Never fails: an absent or broken stanza is Present=false or unknowns.
|
||||
func Parse(reportJSON string) System {
|
||||
out := System{PVEVersion: Unknown, KernelVersion: Unknown,
|
||||
Host: Host{Debian: Unknown, KernelRunning: Unknown, KernelNextBoot: Unknown},
|
||||
Guest: Guest{Debian: Unknown, DockerEngine: Unknown, Containerd: Unknown, LiveRestore: Unknown}}
|
||||
var w wire
|
||||
if json.Unmarshal([]byte(reportJSON), &w) != nil || w.System == nil {
|
||||
return out
|
||||
}
|
||||
out.Present = true
|
||||
out.PVEVersion, out.KernelVersion = orUnknown(w.System.PVEVersion), orUnknown(w.System.KernelVersion)
|
||||
out.VMID, out.FactsError, out.ReadAt = w.System.VMID, w.System.FactsError, w.System.ReadAt
|
||||
var f struct {
|
||||
Host Host `json:"host"`
|
||||
Guest Guest `json:"guest"`
|
||||
}
|
||||
if len(w.System.Facts) > 0 && json.Unmarshal(w.System.Facts, &f) == nil {
|
||||
out.Host, out.Guest = f.Host, f.Guest
|
||||
}
|
||||
out.Host.Debian, out.Host.KernelRunning = orUnknown(out.Host.Debian), orUnknown(out.Host.KernelRunning)
|
||||
out.Host.KernelNextBoot = orUnknown(out.Host.KernelNextBoot)
|
||||
out.Guest.Debian, out.Guest.DockerEngine = orUnknown(out.Guest.Debian), orUnknown(out.Guest.DockerEngine)
|
||||
out.Guest.Containerd, out.Guest.LiveRestore = orUnknown(out.Guest.Containerd), orUnknown(out.Guest.LiveRestore)
|
||||
return out
|
||||
}
|
||||
|
||||
// ShortPVE turns "pve-manager/9.0.11/abc123" into "9.0.11".
|
||||
func ShortPVE(s string) string {
|
||||
if p := strings.Split(s, "/"); len(p) >= 2 && p[0] == "pve-manager" {
|
||||
return p[1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ShortKernel turns "Linux 7.0.14-20-pve #1 SMP …" into "7.0.14-20-pve".
|
||||
func ShortKernel(s string) string {
|
||||
if f := strings.Fields(s); len(f) >= 2 && f[0] == "Linux" {
|
||||
return f[1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NextBootDiffers is true when the box will boot a kernel other than the one it runs (both known).
|
||||
func (s System) NextBootDiffers() bool {
|
||||
return s.Host.KernelRunning != Unknown && s.Host.KernelNextBoot != Unknown && s.Host.KernelRunning != s.Host.KernelNextBoot
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package sysfacts
|
||||
|
||||
import "testing"
|
||||
|
||||
const full = `{"system":{"pve_version":"pve-manager/9.0.11/abc","kernel_version":"Linux 7.0.14-20-pve #1 SMP","vmid":9201,
|
||||
"read_at":"2026-10-04T15:00:00Z","facts":{"host":{"debian":"13.7","kernel_running":"7.0.14-20-pve","kernel_next_boot":"7.0.2-6-pve",
|
||||
"held":["tzdata"],"tainted":4225,"oops_this_boot":true,"kernel_panic":10,"crash_guard":{"armed":false,"tripped":true,
|
||||
"tripped_at":"2026-10-04T16:00:00Z","unclean_boots":["2026-10-04T15:40:00Z"],"unclean_boots_24h":1}},
|
||||
"guest":{"debian":"13.7","docker_engine":"29.7.2","containerd":"2.3.3-1","live_restore":"on"}}}}`
|
||||
|
||||
func TestParse_Full(t *testing.T) {
|
||||
s := Parse(full)
|
||||
if !s.Present || ShortPVE(s.PVEVersion) != "9.0.11" || ShortKernel(s.KernelVersion) != "7.0.14-20-pve" || s.VMID != 9201 {
|
||||
t.Fatalf("%+v", s)
|
||||
}
|
||||
if s.Host.Debian != "13.7" || len(s.Host.Held) != 1 || !*s.Host.OopsThisBoot || !s.Host.CrashGuard.Tripped || !s.NextBootDiffers() {
|
||||
t.Fatalf("host %+v", s.Host)
|
||||
}
|
||||
if s.Guest.DockerEngine != "29.7.2" || s.Guest.LiveRestore != "on" {
|
||||
t.Fatalf("guest %+v", s.Guest)
|
||||
}
|
||||
}
|
||||
|
||||
// An agent older than v0.142.0, or a stanza whose facts failed: unknown everywhere, never empty.
|
||||
// Red-proof: drop the orUnknown on the guest fields and the partial case fails.
|
||||
func TestParse_AbsentAndPartialAreUnknown(t *testing.T) {
|
||||
s := Parse(`{"host":{}}`)
|
||||
if s.Present || s.PVEVersion != Unknown || s.Guest.DockerEngine != Unknown || s.Host.Held != nil {
|
||||
t.Fatalf("absent: %+v", s)
|
||||
}
|
||||
s = Parse(`{"system":{"pve_version":"pve-manager/9.0.11/x","facts_error":"no running customer guest"}}`)
|
||||
if !s.Present || s.Guest.DockerEngine != Unknown || s.Guest.LiveRestore != Unknown || s.Host.KernelRunning != Unknown || s.FactsError == "" {
|
||||
t.Fatalf("partial: %+v", s)
|
||||
}
|
||||
// facts present but empty fields (a guest that answered nothing): still unknown, not ""
|
||||
s = Parse(`{"system":{"pve_version":"x","facts":{"host":{},"guest":{"docker_engine":"","live_restore":"","containerd":""}}}}`)
|
||||
if s.Guest.DockerEngine != Unknown || s.Guest.LiveRestore != Unknown || s.Guest.Containerd != Unknown || s.Host.Debian != Unknown {
|
||||
t.Fatalf("empty facts fields: %+v", s)
|
||||
}
|
||||
if s.NextBootDiffers() {
|
||||
t.Fatal("unknown kernels must not read as 'differs'")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user