package sentinel import ( "encoding/json" "os" "os/exec" "strings" "metis/pkg/facts" ) type nodeConfig struct { USBScratch *usbScratchConfig `json:"usb_scratch,omitempty"` } type usbScratchConfig struct { Mountpoint string `json:"mountpoint,omitempty"` UUID string `json:"uuid,omitempty"` Label string `json:"label,omitempty"` FS string `json:"fs,omitempty"` BindTargets []string `json:"bind_targets,omitempty"` } // Snapshot captures host-level facts. type Snapshot struct { Hostname string `json:"hostname,omitempty"` Kernel string `json:"kernel,omitempty"` OSImage string `json:"os_image,omitempty"` K3sVersion string `json:"k3s_version,omitempty"` Containerd string `json:"containerd,omitempty"` PackageSample map[string]string `json:"package_sample,omitempty"` // small subset to detect drift DropInsSample map[string]string `json:"dropins_sample,omitempty"` // path->content hash/sample USBScratch *facts.USBScratch `json:"usb_scratch,omitempty"` Notes string `json:"notes,omitempty"` } // Collect gathers a minimal set of facts; intended to run inside a DaemonSet pod with host mounts. func Collect() *Snapshot { return &Snapshot{ Hostname: runAndTrim("hostname"), Kernel: runAndTrim("uname", "-r"), OSImage: osRelease(), K3sVersion: runAndTrim("k3s", "version"), Containerd: runAndTrim("containerd", "--version"), PackageSample: pkgSample(), USBScratch: collectUSBScratch(), } } func collectUSBScratch() *facts.USBScratch { raw, err := commandOutput("cat", "/etc/metis/node.json") if err != nil || len(strings.TrimSpace(string(raw))) == 0 { return nil } var cfg nodeConfig if err := json.Unmarshal(raw, &cfg); err != nil || cfg.USBScratch == nil { return nil } desired := cfg.USBScratch scratch := &facts.USBScratch{ Mountpoint: desired.Mountpoint, UUID: desired.UUID, Label: desired.Label, FS: desired.FS, } source, fsType, mounted := mountInfo(desired.Mountpoint) scratch.MountHealthy = mounted && strings.TrimSpace(source) != "" if scratch.MountHealthy && desired.FS != "" && fsType != "" { scratch.MountHealthy = strings.EqualFold(fsType, desired.FS) } if scratch.FS == "" && fsType != "" { scratch.FS = fsType } device := source if device == "" && desired.UUID != "" { device = resolveDeviceByUUID(desired.UUID) } if device == "" && desired.Label != "" { device = resolveDeviceByLabel(desired.Label) } if device != "" { export := blkidExport(device) if desired.UUID != "" { scratch.UUIDHealthy = export["UUID"] == desired.UUID } if desired.Label != "" { scratch.LabelHealthy = export["LABEL"] == desired.Label } if scratch.FS == "" { scratch.FS = export["TYPE"] } } healthy := true if len(desired.BindTargets) > 0 { scratch.BindTargets = make([]facts.USBBindTarget, 0, len(desired.BindTargets)) for _, target := range desired.BindTargets { ok := bindHealthy(target, desired.Mountpoint) if !ok { healthy = false } scratch.BindTargets = append(scratch.BindTargets, facts.USBBindTarget{ Path: target, Healthy: ok, }) } scratch.BindHealthy = healthy } else { scratch.BindHealthy = true } return scratch } func runAndTrim(cmd string, args ...string) string { out, err := commandOutput(cmd, args...) if err != nil { return "" } return strings.TrimSpace(string(out)) } func osRelease() string { out, err := commandOutput("cat", "/etc/os-release") if err != nil { return "" } for _, line := range strings.Split(string(out), "\n") { if strings.HasPrefix(line, "PRETTY_NAME=") { return strings.Trim(line[len("PRETTY_NAME="):], "\"") } } return "" } // pkgSample grabs a tiny subset of package versions to detect drift without collecting everything. func pkgSample() map[string]string { names := []string{"containerd", "k3s", "nvidia-container-toolkit", "linux-image-raspi"} result := map[string]string{} for _, n := range names { v := pkgVersion(n) if v != "" { result[n] = v } } return result } func pkgVersion(name string) string { // Try dpkg-query first. out, err := commandOutput("dpkg-query", "-W", "-f", "${Version}", name) if err == nil && len(out) > 0 { return strings.TrimSpace(string(out)) } // Fallback rpm. out, err = commandOutput("rpm", "-q", "--qf", "%{VERSION}-%{RELEASE}", name) if err == nil && len(out) > 0 { return strings.TrimSpace(string(out)) } return "" } func mountInfo(target string) (string, string, bool) { target = strings.TrimSpace(target) if target == "" { return "", "", false } out, err := commandOutput("findmnt", "-P", "-n", "-T", target, "-o", "SOURCE,TARGET,FSTYPE") if err != nil { return "", "", false } fields := parseKeyValues(string(out)) source := fields["SOURCE"] fsType := fields["FSTYPE"] return source, fsType, strings.TrimSpace(fields["TARGET"]) == target } func bindHealthy(target, source string) bool { target = strings.TrimSpace(target) source = strings.TrimSpace(source) if target == "" || source == "" { return false } mountSource, _, mounted := mountInfo(target) return mounted && strings.TrimSpace(mountSource) == source } func resolveDeviceByUUID(uuid string) string { uuid = strings.TrimSpace(uuid) if uuid == "" { return "" } return runAndTrim("blkid", "-U", uuid) } func resolveDeviceByLabel(label string) string { label = strings.TrimSpace(label) if label == "" { return "" } return runAndTrim("blkid", "-L", label) } func blkidExport(device string) map[string]string { device = strings.TrimSpace(device) if device == "" { return map[string]string{} } out, err := commandOutput("blkid", "-o", "export", device) if err != nil { return map[string]string{} } return parseKeyValues(string(out)) } func parseKeyValues(out string) map[string]string { result := map[string]string{} for _, field := range strings.Fields(strings.TrimSpace(out)) { key, value, ok := strings.Cut(field, "=") if !ok { continue } result[key] = strings.Trim(value, `"`) } return result } func commandOutput(cmd string, args ...string) ([]byte, error) { if os.Getenv("METIS_SENTINEL_NSENTER") == "1" { nsenterArgs := []string{"-t", "1", "-m", "-u", "-n", "-i", "-p", "--", cmd} nsenterArgs = append(nsenterArgs, args...) return exec.Command("nsenter", nsenterArgs...).Output() } return exec.Command(cmd, args...).Output() }