metis/pkg/sentinel/collector.go

89 lines
2.6 KiB
Go

package sentinel
import (
"os"
"os/exec"
"strings"
)
// 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
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(),
}
}
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 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()
}