Align hardware export with ingest contract
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bee/audit/internal/schema"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type sensorsDoc map[string]map[string]any
|
||||
|
||||
func collectSensors() *schema.HardwareSensors {
|
||||
doc, err := readSensorsJSONDoc()
|
||||
if err != nil {
|
||||
slog.Info("sensors: unavailable, skipping", "err", err)
|
||||
return nil
|
||||
}
|
||||
sensors := buildSensorsFromDoc(doc)
|
||||
if sensors == nil || (len(sensors.Fans) == 0 && len(sensors.Power) == 0 && len(sensors.Temperatures) == 0 && len(sensors.Other) == 0) {
|
||||
return nil
|
||||
}
|
||||
slog.Info("sensors: collected",
|
||||
"fans", len(sensors.Fans),
|
||||
"power", len(sensors.Power),
|
||||
"temperatures", len(sensors.Temperatures),
|
||||
"other", len(sensors.Other),
|
||||
)
|
||||
return sensors
|
||||
}
|
||||
|
||||
func readSensorsJSONDoc() (sensorsDoc, error) {
|
||||
out, err := exec.Command("sensors", "-j").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc sensorsDoc
|
||||
if err := json.Unmarshal(out, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func buildSensorsFromDoc(doc sensorsDoc) *schema.HardwareSensors {
|
||||
if len(doc) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := &schema.HardwareSensors{}
|
||||
seen := map[string]struct{}{}
|
||||
|
||||
chips := make([]string, 0, len(doc))
|
||||
for chip := range doc {
|
||||
chips = append(chips, chip)
|
||||
}
|
||||
sort.Strings(chips)
|
||||
|
||||
for _, chip := range chips {
|
||||
features := doc[chip]
|
||||
location := sensorLocation(chip)
|
||||
|
||||
keys := make([]string, 0, len(features))
|
||||
for key := range features {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
if strings.EqualFold(key, "Adapter") {
|
||||
continue
|
||||
}
|
||||
feature, ok := features[key].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(key)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
switch classifySensorFeature(feature) {
|
||||
case "fan":
|
||||
item := buildFanSensor(name, location, feature)
|
||||
if item == nil || duplicateSensor(seen, "fan", item.Name) {
|
||||
continue
|
||||
}
|
||||
result.Fans = append(result.Fans, *item)
|
||||
case "temp":
|
||||
item := buildTempSensor(name, location, feature)
|
||||
if item == nil || duplicateSensor(seen, "temp", item.Name) {
|
||||
continue
|
||||
}
|
||||
result.Temperatures = append(result.Temperatures, *item)
|
||||
case "power":
|
||||
item := buildPowerSensor(name, location, feature)
|
||||
if item == nil || duplicateSensor(seen, "power", item.Name) {
|
||||
continue
|
||||
}
|
||||
result.Power = append(result.Power, *item)
|
||||
default:
|
||||
item := buildOtherSensor(name, location, feature)
|
||||
if item == nil || duplicateSensor(seen, "other", item.Name) {
|
||||
continue
|
||||
}
|
||||
result.Other = append(result.Other, *item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func parseSensorsJSON(raw []byte) (*schema.HardwareSensors, error) {
|
||||
var doc sensorsDoc
|
||||
err := json.Unmarshal(raw, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildSensorsFromDoc(doc), nil
|
||||
}
|
||||
|
||||
func duplicateSensor(seen map[string]struct{}, sensorType, name string) bool {
|
||||
key := sensorType + "\x00" + name
|
||||
if _, ok := seen[key]; ok {
|
||||
return true
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
return false
|
||||
}
|
||||
|
||||
func sensorLocation(chip string) *string {
|
||||
chip = strings.TrimSpace(chip)
|
||||
if chip == "" {
|
||||
return nil
|
||||
}
|
||||
return &chip
|
||||
}
|
||||
|
||||
func classifySensorFeature(feature map[string]any) string {
|
||||
for key := range feature {
|
||||
switch {
|
||||
case strings.Contains(key, "fan") && strings.HasSuffix(key, "_input"):
|
||||
return "fan"
|
||||
case strings.Contains(key, "temp") && strings.HasSuffix(key, "_input"):
|
||||
return "temp"
|
||||
case strings.Contains(key, "power") && (strings.HasSuffix(key, "_input") || strings.HasSuffix(key, "_average")):
|
||||
return "power"
|
||||
case strings.Contains(key, "curr") && strings.HasSuffix(key, "_input"):
|
||||
return "power"
|
||||
case strings.HasPrefix(key, "in") && strings.HasSuffix(key, "_input"):
|
||||
return "power"
|
||||
}
|
||||
}
|
||||
return "other"
|
||||
}
|
||||
|
||||
func buildFanSensor(name string, location *string, feature map[string]any) *schema.HardwareFanSensor {
|
||||
rpm, ok := firstFeatureInt(feature, "_input")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item := &schema.HardwareFanSensor{Name: name, Location: location, RPM: &rpm}
|
||||
if status := sensorStatusFromFeature(feature); status != nil {
|
||||
item.Status = status
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func buildTempSensor(name string, location *string, feature map[string]any) *schema.HardwareTemperatureSensor {
|
||||
celsius, ok := firstFeatureFloat(feature, "_input")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item := &schema.HardwareTemperatureSensor{Name: name, Location: location, Celsius: &celsius}
|
||||
if warning, ok := firstFeatureFloatWithSuffixes(feature, []string{"_max", "_high"}); ok {
|
||||
item.ThresholdWarningCelsius = &warning
|
||||
}
|
||||
if critical, ok := firstFeatureFloatWithSuffixes(feature, []string{"_crit", "_emergency"}); ok {
|
||||
item.ThresholdCriticalCelsius = &critical
|
||||
}
|
||||
if status := sensorStatusFromFeature(feature); status != nil {
|
||||
item.Status = status
|
||||
} else {
|
||||
item.Status = deriveTemperatureStatus(item.Celsius, item.ThresholdWarningCelsius, item.ThresholdCriticalCelsius)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func buildPowerSensor(name string, location *string, feature map[string]any) *schema.HardwarePowerSensor {
|
||||
item := &schema.HardwarePowerSensor{Name: name, Location: location}
|
||||
if v, ok := firstFeatureFloatWithContains(feature, []string{"power"}); ok {
|
||||
item.PowerW = &v
|
||||
}
|
||||
if v, ok := firstFeatureFloatWithPrefix(feature, "curr"); ok {
|
||||
item.CurrentA = &v
|
||||
}
|
||||
if v, ok := firstFeatureFloatWithPrefix(feature, "in"); ok {
|
||||
item.VoltageV = &v
|
||||
}
|
||||
if item.PowerW == nil && item.CurrentA == nil && item.VoltageV == nil {
|
||||
return nil
|
||||
}
|
||||
if status := sensorStatusFromFeature(feature); status != nil {
|
||||
item.Status = status
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func buildOtherSensor(name string, location *string, feature map[string]any) *schema.HardwareOtherSensor {
|
||||
value, unit, ok := firstGenericSensorValue(feature)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
item := &schema.HardwareOtherSensor{Name: name, Location: location, Value: &value}
|
||||
if unit != "" {
|
||||
item.Unit = &unit
|
||||
}
|
||||
if status := sensorStatusFromFeature(feature); status != nil {
|
||||
item.Status = status
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func sensorStatusFromFeature(feature map[string]any) *string {
|
||||
for key, raw := range feature {
|
||||
if !strings.HasSuffix(key, "_alarm") {
|
||||
continue
|
||||
}
|
||||
if number, ok := floatFromAny(raw); ok && number > 0 {
|
||||
status := statusWarning
|
||||
return &status
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deriveTemperatureStatus(current, warning, critical *float64) *string {
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case critical != nil && *current >= *critical:
|
||||
status := statusCritical
|
||||
return &status
|
||||
case warning != nil && *current >= *warning:
|
||||
status := statusWarning
|
||||
return &status
|
||||
default:
|
||||
status := statusOK
|
||||
return &status
|
||||
}
|
||||
}
|
||||
|
||||
func firstFeatureInt(feature map[string]any, suffix string) (int, bool) {
|
||||
for key, raw := range feature {
|
||||
if strings.HasSuffix(key, suffix) {
|
||||
if value, ok := floatFromAny(raw); ok {
|
||||
return int(value), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstFeatureFloat(feature map[string]any, suffix string) (float64, bool) {
|
||||
return firstFeatureFloatWithSuffixes(feature, []string{suffix})
|
||||
}
|
||||
|
||||
func firstFeatureFloatWithSuffixes(feature map[string]any, suffixes []string) (float64, bool) {
|
||||
keys := sortedFeatureKeys(feature)
|
||||
for _, key := range keys {
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(key, suffix) {
|
||||
if value, ok := floatFromAny(feature[key]); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstFeatureFloatWithContains(feature map[string]any, parts []string) (float64, bool) {
|
||||
keys := sortedFeatureKeys(feature)
|
||||
for _, key := range keys {
|
||||
matched := true
|
||||
for _, part := range parts {
|
||||
if !strings.Contains(key, part) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
if value, ok := floatFromAny(feature[key]); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstFeatureFloatWithPrefix(feature map[string]any, prefix string) (float64, bool) {
|
||||
keys := sortedFeatureKeys(feature)
|
||||
for _, key := range keys {
|
||||
if strings.HasPrefix(key, prefix) && strings.HasSuffix(key, "_input") {
|
||||
if value, ok := floatFromAny(feature[key]); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func firstGenericSensorValue(feature map[string]any) (float64, string, bool) {
|
||||
keys := sortedFeatureKeys(feature)
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key, "_alarm") {
|
||||
continue
|
||||
}
|
||||
value, ok := floatFromAny(feature[key])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
unit := inferSensorUnit(key)
|
||||
return value, unit, true
|
||||
}
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
func inferSensorUnit(key string) string {
|
||||
switch {
|
||||
case strings.Contains(key, "humidity"):
|
||||
return "%"
|
||||
case strings.Contains(key, "intrusion"):
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sortedFeatureKeys(feature map[string]any) []string {
|
||||
keys := make([]string, 0, len(feature))
|
||||
for key := range feature {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func floatFromAny(raw any) (float64, bool) {
|
||||
switch value := raw.(type) {
|
||||
case float64:
|
||||
return value, true
|
||||
case float32:
|
||||
return float64(value), true
|
||||
case int:
|
||||
return float64(value), true
|
||||
case int64:
|
||||
return float64(value), true
|
||||
case json.Number:
|
||||
if f, err := value.Float64(); err == nil {
|
||||
return f, true
|
||||
}
|
||||
case string:
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
if f, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user