// 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"` } // ConfigBundle is the box's root-owned config bundle (R-840, agent v0.143.0): the agent's own read of the record // (version "none" = no bundle ever reached the box), with the drift the wrapper's facts add (files changed by hand). type ConfigBundle struct { Version string `json:"version"` // agent version of the bundle | none | unknown BundleSHA256 string `json:"bundle_sha256"` InstalledAt string `json:"installed_at"` Drift []string `json:"drift"` } // 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 Bundle ConfigBundle } 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"` ConfigBundle *ConfigBundle `json:"config_bundle"` } `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 struct { Host ConfigBundle *ConfigBundle `json:"config_bundle"` } `json:"host"` Guest Guest `json:"guest"` } out.Bundle = ConfigBundle{Version: Unknown} if w.System.ConfigBundle != nil && w.System.ConfigBundle.Version != "" { out.Bundle = *w.System.ConfigBundle } if len(w.System.Facts) > 0 && json.Unmarshal(w.System.Facts, &f) == nil { out.Host, out.Guest = f.Host.Host, f.Guest // The wrapper's view adds the drift; its record is the same file the agent read. if fb := f.Host.ConfigBundle; fb != nil && fb.Version != "" && fb.Version != Unknown { if out.Bundle.Version == Unknown { out.Bundle = *fb } out.Bundle.Drift = fb.Drift } } 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 }