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") }