package host import ( "context" "errors" "fmt" "math" "sort" "strings" ) const ( CapabilityThermal = "host.thermal" CapabilityFans = "host.fans" CapabilityGPU = "host.gpu" ) type HardwareLimits struct { MaxTemperatures int MaxFans int MaxGPUs int MaxCapabilities int } func (l HardwareLimits) withDefaults() HardwareLimits { if l.MaxTemperatures == 0 { l.MaxTemperatures = 256 } if l.MaxFans == 0 { l.MaxFans = 256 } if l.MaxGPUs == 0 { l.MaxGPUs = 16 } if l.MaxCapabilities == 0 { l.MaxCapabilities = 32 } return l } func (l HardwareLimits) Validate() error { if l.MaxTemperatures < 1 || l.MaxTemperatures > 512 || l.MaxFans < 1 || l.MaxFans > 512 || l.MaxGPUs < 1 || l.MaxGPUs > 64 || l.MaxCapabilities < 1 || l.MaxCapabilities > 100 { return errors.New("hardware limits are outside safe bounds") } return nil } type ThermalPolicy struct { AttentionCelsius float64 CriticalCelsius float64 } func (p ThermalPolicy) withDefaults() ThermalPolicy { if p.AttentionCelsius == 0 { p.AttentionCelsius = 75 } if p.CriticalCelsius == 0 { p.CriticalCelsius = 85 } return p } func (p ThermalPolicy) Validate() error { if p.AttentionCelsius <= 0 || p.CriticalCelsius <= p.AttentionCelsius || p.CriticalCelsius > 150 { return errors.New("thermal policy is outside safe bounds") } return nil } type Capability struct { ID string `json:"id"` Version string `json:"version"` State string `json:"state"` Reason string `json:"reason,omitempty"` } type RawTemperature struct { ID string `json:"id,omitempty"` Name string `json:"name"` Celsius float64 `json:"celsius"` } type Temperature struct { ID string `json:"id"` Name string `json:"name"` Celsius float64 `json:"celsius"` } type RawFan struct { ID string `json:"id,omitempty"` Name string `json:"name"` RPM int `json:"rpm"` } type Fan struct { ID string `json:"id"` Name string `json:"name"` RPM int `json:"rpm"` } type RawGPU struct { ID string `json:"id,omitempty"` Name string `json:"name"` Vendor string `json:"vendor,omitempty"` Utilization *float64 `json:"utilizationPercent,omitempty"` MemoryUsedBytes uint64 `json:"memoryUsedBytes,omitempty"` MemoryTotalBytes uint64 `json:"memoryTotalBytes,omitempty"` TemperatureCelsius *float64 `json:"temperatureCelsius,omitempty"` } type GPU struct { ID string `json:"id"` Name string `json:"name"` Vendor string `json:"vendor,omitempty"` Utilization *float64 `json:"utilizationPercent,omitempty"` MemoryUsedBytes uint64 `json:"memoryUsedBytes,omitempty"` MemoryTotalBytes uint64 `json:"memoryTotalBytes,omitempty"` MemoryUtilizationPercent float64 `json:"memoryUtilizationPercent,omitempty"` TemperatureCelsius *float64 `json:"temperatureCelsius,omitempty"` } type RawHardware struct { Capabilities []Capability `json:"capabilities"` Temperatures []RawTemperature `json:"temperatures"` Fans []RawFan `json:"fans"` GPUs []RawGPU `json:"gpus"` } type HardwareStatus struct { State string `json:"state"` Reasons []StatusReason `json:"reasons,omitempty"` } type HardwareSnapshot struct { Capabilities []Capability `json:"capabilities"` Temperatures []Temperature `json:"temperatures"` Fans []Fan `json:"fans"` GPUs []GPU `json:"gpus"` Status HardwareStatus `json:"status"` } type HardwareSource interface { Hardware(context.Context) (RawHardware, error) } type HardwareAdapter struct { Source HardwareSource Limits HardwareLimits Policy ThermalPolicy } func (a HardwareAdapter) Snapshot(ctx context.Context, sourceID string) (HardwareSnapshot, error) { if err := ctx.Err(); err != nil { return HardwareSnapshot{}, err } if a.Source == nil { return DisabledHardware(), nil } raw, err := a.Source.Hardware(ctx) if err != nil { return HardwareSnapshot{}, err } return NormalizeHardware(raw, sourceID, a.Limits, a.Policy) } func DisabledHardware() HardwareSnapshot { capabilities := []Capability{ {ID: CapabilityThermal, Version: ContractVersion, State: "disabled", Reason: "capability_absent"}, {ID: CapabilityFans, Version: ContractVersion, State: "disabled", Reason: "capability_absent"}, {ID: CapabilityGPU, Version: ContractVersion, State: "disabled", Reason: "capability_absent"}, } return HardwareSnapshot{Capabilities: capabilities, Status: HardwareStatus{State: StatusUnknown, Reasons: []StatusReason{{Code: "hardware_capability_absent", Message: "Optionele hardwaretelemetrie is niet beschikbaar."}}}} } func NormalizeHardware(raw RawHardware, sourceID string, limits HardwareLimits, policy ThermalPolicy) (HardwareSnapshot, error) { limits = limits.withDefaults() policy = policy.withDefaults() if err := limits.Validate(); err != nil { return HardwareSnapshot{}, err } if err := policy.Validate(); err != nil { return HardwareSnapshot{}, err } if len(raw.Temperatures) > limits.MaxTemperatures || len(raw.Fans) > limits.MaxFans || len(raw.GPUs) > limits.MaxGPUs || len(raw.Capabilities) > limits.MaxCapabilities { return HardwareSnapshot{}, errors.New("hardware collection exceeds bounds") } capabilities := normalizeCapabilities(raw.Capabilities) for _, required := range []string{CapabilityThermal, CapabilityFans, CapabilityGPU} { if _, ok := capabilityByID(capabilities, required); !ok { capabilities = append(capabilities, Capability{ID: required, Version: ContractVersion, State: "disabled", Reason: "capability_absent"}) } } sort.Slice(capabilities, func(i, j int) bool { return capabilities[i].ID < capabilities[j].ID }) temperatures := make([]Temperature, 0, len(raw.Temperatures)) for _, item := range raw.Temperatures { if err := validateName(item.Name, 128); err != nil || math.IsNaN(item.Celsius) || math.IsInf(item.Celsius, 0) || item.Celsius < -100 || item.Celsius > 150 { return HardwareSnapshot{}, errors.New("invalid temperature sensor") } temperatures = append(temperatures, Temperature{ID: stableSensorID(sourceID, "temperature", item.ID, item.Name), Name: item.Name, Celsius: item.Celsius}) } sort.Slice(temperatures, func(i, j int) bool { return temperatures[i].ID < temperatures[j].ID }) fans := make([]Fan, 0, len(raw.Fans)) for _, item := range raw.Fans { if err := validateName(item.Name, 128); err != nil || item.RPM < 0 || item.RPM > 100000 { return HardwareSnapshot{}, errors.New("invalid fan sensor") } fans = append(fans, Fan{ID: stableSensorID(sourceID, "fan", item.ID, item.Name), Name: item.Name, RPM: item.RPM}) } sort.Slice(fans, func(i, j int) bool { return fans[i].ID < fans[j].ID }) gpus := make([]GPU, 0, len(raw.GPUs)) for _, item := range raw.GPUs { if err := validateName(item.Name, 128); err != nil { return HardwareSnapshot{}, errors.New("invalid GPU name") } if item.Utilization != nil && (*item.Utilization < 0 || *item.Utilization > 100 || math.IsNaN(*item.Utilization) || math.IsInf(*item.Utilization, 0)) { return HardwareSnapshot{}, errors.New("invalid GPU utilization") } if item.MemoryTotalBytes > 0 && item.MemoryUsedBytes > item.MemoryTotalBytes { return HardwareSnapshot{}, errors.New("invalid GPU memory") } if item.TemperatureCelsius != nil && (*item.TemperatureCelsius < -100 || *item.TemperatureCelsius > 150 || math.IsNaN(*item.TemperatureCelsius) || math.IsInf(*item.TemperatureCelsius, 0)) { return HardwareSnapshot{}, errors.New("invalid GPU temperature") } gpu := GPU{ID: stableSensorID(sourceID, "gpu", item.ID, item.Name), Name: item.Name, Vendor: item.Vendor, Utilization: cloneFloat(item.Utilization), MemoryUsedBytes: item.MemoryUsedBytes, MemoryTotalBytes: item.MemoryTotalBytes, TemperatureCelsius: cloneFloat(item.TemperatureCelsius)} if item.MemoryTotalBytes > 0 { gpu.MemoryUtilizationPercent = float64(item.MemoryUsedBytes) / float64(item.MemoryTotalBytes) * 100 } gpus = append(gpus, gpu) } sort.Slice(gpus, func(i, j int) bool { return gpus[i].ID < gpus[j].ID }) status := evaluateHardwareStatus(temperatures, gpus, policy) return HardwareSnapshot{Capabilities: capabilities, Temperatures: temperatures, Fans: fans, GPUs: gpus, Status: status}, nil } func evaluateHardwareStatus(temperatures []Temperature, gpus []GPU, policy ThermalPolicy) HardwareStatus { result := HardwareStatus{State: StatusHealthy} max := -math.MaxFloat64 sensor := "" for _, item := range temperatures { if item.Celsius > max { max = item.Celsius sensor = item.Name } } for _, item := range gpus { if item.TemperatureCelsius != nil && *item.TemperatureCelsius > max { max = *item.TemperatureCelsius sensor = item.Name } } if sensor == "" { return HardwareStatus{State: StatusUnknown, Reasons: []StatusReason{{Code: "thermal_data_absent", Message: "Er is geen betrouwbare temperatuursensor beschikbaar."}}} } if max >= policy.CriticalCelsius { return HardwareStatus{State: "critical", Reasons: []StatusReason{{Code: "thermal_critical", Message: fmt.Sprintf("Temperatuur %.1f °C bij %s overschrijdt de kritieke grens.", max, sensor)}}} } if max >= policy.AttentionCelsius { return HardwareStatus{State: StatusDegraded, Reasons: []StatusReason{{Code: "thermal_attention", Message: fmt.Sprintf("Temperatuur %.1f °C bij %s vraagt aandacht.", max, sensor)}}} } return result } func normalizeCapabilities(values []Capability) []Capability { result := make([]Capability, 0, len(values)) seen := map[string]bool{} for _, value := range values { if value.ID == "" || seen[value.ID] { continue } if value.Version == "" { value.Version = ContractVersion } if value.State == "" { value.State = "unavailable" } seen[value.ID] = true result = append(result, value) } return result } func capabilityByID(values []Capability, id string) (Capability, bool) { for _, value := range values { if value.ID == id { return value, true } } return Capability{}, false } func validateName(value string, max int) error { if strings.TrimSpace(value) == "" || len(value) > max { return errors.New("name is empty or too long") } return nil } func stableSensorID(sourceID, kind, externalID, name string) string { identity := strings.TrimSpace(externalID) if identity == "" { identity = strings.TrimSpace(name) } identity = strings.ToLower(strings.TrimSpace(identity)) identity = strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-").Replace(identity) return strings.ToLower(strings.TrimSpace(sourceID)) + "/" + kind + "/" + identity }