// 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"` } // DiskTrim is the agent's weekly guest disk trim stanza (R-444, `09` §3 decision 139): the host report's TOP-LEVEL // `guest_disk_trim` object (agent internal/hub GuestDiskTrimStatus). A field-by-field mirror of the agent's type, // checked BOTH ways by the wire-contract gate (SUBTREE_MIRRORS in scripts/wire_contract_gate.py), so a rename or a new // field on either side convicts. type DiskTrim struct { Schedule string `json:"schedule"` Guests []GuestTrim `json:"guests"` } // GuestTrim is one guest's LAST trim attempt. `ok` with `last_attempt_at` is that attempt's verdict; `last_ok_at` is // the last attempt that succeeded ("" = never) — staleness is judged on it, never on the attempt time alone. type GuestTrim struct { VMID int `json:"vmid"` LastAttemptAt string `json:"last_attempt_at"` // RFC3339 OK bool `json:"ok"` BytesTrimmed int64 `json:"bytes_trimmed"` Mounts int `json:"mounts"` DurationSeconds float64 `json:"duration_seconds"` LastOKAt string `json:"last_ok_at"` // RFC3339, "" = never Error string `json:"error"` } // 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 // DiskTrim is the top-level `guest_disk_trim` stanza (R-444). Independent of the `system` stanza: it is read even // when Present is false. nil = the agent sent none (an agent with no trimmer). DiskTrim *DiskTrim } type wire struct { DiskTrim *DiskTrim `json:"guest_disk_trim"` 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 { return out } out.DiskTrim = w.DiskTrim if 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 }