Public source validation / validate (push) Failing after 3m8s
301 lines
10 KiB
Go
301 lines
10 KiB
Go
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
|
|
}
|