a411cde7c4
sysfacts reads the agent's top-level guest_disk_trim stanza (schedule + per-guest last attempt: vmid, last_attempt_at, ok, bytes_trimmed, mounts, duration_seconds, last_ok_at, error) into a field-by-field mirror. The System page's new 'Last disk trim' column shows the last successful trim and the GiB it freed; amber when the newest attempt failed (error shown) or last_ok_at is older than 14 days (judged on the success time, never the attempt time); '—' when the agent sends no stanza. wire_contract_gate: SUBTREE_MIRRORS checks guest_disk_trim field by field BOTH ways against sysfacts.DiskTrim; decoys (ok renamed, last_ok_at dropped) in test_gate_decoys.py. Decision 139. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
184 lines
7.2 KiB
Go
184 lines
7.2 KiB
Go
// 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
|
|
}
|