152 lines
3.9 KiB
Go
152 lines
3.9 KiB
Go
package fleet
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// ReconReport holds read-only host capability probes for triple-onion deploy.
|
|
type ReconReport struct {
|
|
Kernel string `json:"kernel"`
|
|
Arch string `json:"arch"`
|
|
CgroupsV2 bool `json:"cgroups_v2"`
|
|
PodmanAvailable bool `json:"podman_available"`
|
|
DockerAvailable bool `json:"docker_available"`
|
|
GPU GPUInfo `json:"gpu"`
|
|
Virt string `json:"virt,omitempty"`
|
|
ContainerRuntime string `json:"container_runtime,omitempty"`
|
|
Extra map[string]string `json:"extra,omitempty"`
|
|
}
|
|
|
|
type GPUInfo struct {
|
|
Available bool `json:"available"`
|
|
Vendor string `json:"vendor,omitempty"`
|
|
Devices []string `json:"devices,omitempty"`
|
|
}
|
|
|
|
// RunRecon executes read-only probes: kernel, cgroups, podman, GPU.
|
|
func RunRecon() (*ReconReport, error) {
|
|
report := &ReconReport{Extra: map[string]string{}}
|
|
|
|
if out, err := exec.Command("uname", "-r").Output(); err == nil {
|
|
report.Kernel = strings.TrimSpace(string(out))
|
|
}
|
|
if out, err := exec.Command("uname", "-m").Output(); err == nil {
|
|
report.Arch = strings.TrimSpace(string(out))
|
|
}
|
|
|
|
report.CgroupsV2 = probeCgroupsV2()
|
|
report.PodmanAvailable = commandExists("podman")
|
|
report.DockerAvailable = commandExists("docker")
|
|
report.GPU = probeGPU()
|
|
report.Virt = probeVirt()
|
|
report.ContainerRuntime = detectContainerRuntime(report)
|
|
|
|
return report, nil
|
|
}
|
|
|
|
func probeCgroupsV2() bool {
|
|
data, err := os.ReadFile("/sys/fs/cgroup/cgroup.controllers")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return len(strings.TrimSpace(string(data))) > 0
|
|
}
|
|
|
|
func probeGPU() GPUInfo {
|
|
info := GPUInfo{}
|
|
if commandExists("nvidia-smi") {
|
|
if out, err := exec.Command("nvidia-smi", "-L").Output(); err == nil {
|
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
|
for _, line := range lines {
|
|
if line != "" {
|
|
info.Devices = append(info.Devices, line)
|
|
}
|
|
}
|
|
if len(info.Devices) > 0 {
|
|
info.Available = true
|
|
info.Vendor = "nvidia"
|
|
}
|
|
}
|
|
}
|
|
if !info.Available && commandExists("rocm-smi") {
|
|
if _, err := exec.Command("rocm-smi", "--showid").Output(); err == nil {
|
|
info.Available = true
|
|
info.Vendor = "amd"
|
|
}
|
|
}
|
|
return info
|
|
}
|
|
|
|
func probeVirt() string {
|
|
data, err := os.ReadFile("/sys/class/dmi/id/product_name")
|
|
if err != nil {
|
|
return "baremetal"
|
|
}
|
|
name := strings.ToLower(strings.TrimSpace(string(data)))
|
|
switch {
|
|
case strings.Contains(name, "vmware"), strings.Contains(name, "virtualbox"),
|
|
strings.Contains(name, "kvm"), strings.Contains(name, "qemu"):
|
|
return "vm"
|
|
default:
|
|
return "baremetal"
|
|
}
|
|
}
|
|
|
|
func detectContainerRuntime(r *ReconReport) string {
|
|
if r.PodmanAvailable {
|
|
return "podman"
|
|
}
|
|
if r.DockerAvailable {
|
|
return "docker"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func commandExists(name string) bool {
|
|
_, err := exec.LookPath(name)
|
|
return err == nil
|
|
}
|
|
|
|
// PhenotypeFromRecon builds a stable fingerprint key from recon data.
|
|
func PhenotypeFromRecon(r *ReconReport) string {
|
|
gpu := "none"
|
|
if r.GPU.Available {
|
|
gpu = r.GPU.Vendor
|
|
}
|
|
rt := r.ContainerRuntime
|
|
if rt == "" {
|
|
rt = "none"
|
|
}
|
|
parts := []string{r.Arch, r.Virt, gpu, rt}
|
|
key := strings.Join(parts, "|")
|
|
// Normalize kernel major for grouping
|
|
if idx := strings.Index(r.Kernel, "."); idx > 0 {
|
|
key = r.Kernel[:idx] + "." + strings.Split(r.Kernel[idx+1:], ".")[0] + "|" + key
|
|
}
|
|
return key
|
|
}
|
|
|
|
// ReconJSON serializes a recon report for lotl metadata.
|
|
func ReconJSON(r *ReconReport) string {
|
|
b, _ := json.Marshal(r)
|
|
return string(b)
|
|
}
|
|
|
|
// ParseReconReport loads recon from JSON metadata.
|
|
func ParseReconReport(raw string) (*ReconReport, error) {
|
|
var r ReconReport
|
|
if err := json.Unmarshal([]byte(raw), &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// HostReconPath returns optional cached recon file for a host.
|
|
func HostReconPath(dataDir, hostID string) string {
|
|
return filepath.Join(dataDir, "recon", hostID+".json")
|
|
}
|