Files
core/internal/ingest/lot_classifier.go

264 lines
6.0 KiB
Go

package ingest
import (
"context"
"database/sql"
"fmt"
"regexp"
"strconv"
"strings"
"reanimator/internal/idgen"
)
const (
mysqlErrDuplicateKey = 1062
)
var lotNormalizePattern = regexp.MustCompile(`[^0-9A-Z]+`)
func (s *Service) ensureLotByCode(ctx context.Context, tx *sql.Tx, code string, description *string) (string, error) {
if code == "" {
return "", nil
}
var id string
err := tx.QueryRowContext(ctx,
`SELECT id FROM lots WHERE code = ? LIMIT 1`,
code,
).Scan(&id)
if err == nil {
return id, nil
}
if err != sql.ErrNoRows {
return "", err
}
var descriptionValue interface{}
if description != nil {
descriptionValue = *description
}
lotID, err := s.idgen.Generate(ctx, idgen.Lot)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx,
`INSERT INTO lots (id, code, description) VALUES (?, ?, ?)`,
lotID,
code,
descriptionValue,
)
if err != nil {
if mysqlErrorNumber(err) == mysqlErrDuplicateKey {
if err := tx.QueryRowContext(ctx,
`SELECT id FROM lots WHERE code = ? LIMIT 1`,
code,
).Scan(&id); err != nil {
return "", err
}
return id, nil
}
return "", err
}
return lotID, nil
}
func classifyLot(component HardwareComponent) (string, *string) {
switch component.ComponentType {
case "cpu":
return classifyCpuLot(component)
case "memory":
return classifyMemoryLot(component)
case "storage":
return classifyStorageLot(component)
case "psu":
return classifyPsULot(component)
case "pcie":
return classifyPCIELot(component)
default:
return "", nil
}
}
func classifyCpuLot(component HardwareComponent) (string, *string) {
vendor := normalizeLotPointer(component.Vendor)
model := normalizeLotPointer(component.Model)
if vendor == "" && model == "" {
return "", nil
}
parts := []string{"CPU"}
if vendor != "" {
parts = append(parts, vendor)
}
if model != "" {
parts = append(parts, model)
}
code := strings.Join(parts, "_")
var desc *string
if component.Model != nil {
desc = component.Model
} else if component.Vendor != nil {
desc = component.Vendor
}
return code, desc
}
func classifyMemoryLot(component HardwareComponent) (string, *string) {
slotType := normalizeLotStringFromAttributes(component.Attributes, "type")
sizeMB, ok := floatAttribute(component.Attributes, "size_mb")
if !ok || sizeMB <= 0 {
return "", nil
}
sizeGB := int(sizeMB / 1024)
if sizeGB <= 0 {
sizeGB = 1
}
if slotType == "" {
slotType = "UNKNOWN"
}
code := fmt.Sprintf("DIMM_%s_%dGB", slotType, sizeGB)
return code, component.Model
}
func classifyStorageLot(component HardwareComponent) (string, *string) {
driveType := normalizeLotStringFromAttributes(component.Attributes, "type")
interfaceType := normalizeLotStringFromAttributes(component.Attributes, "interface")
sizeGB, ok := floatAttribute(component.Attributes, "size_gb")
if !ok || sizeGB <= 0 {
return "", nil
}
sizeLabel := formatStorageSize(sizeGB)
if driveType == "" {
driveType = "STORAGE"
}
parts := []string{driveType}
if interfaceType != "" {
parts = append(parts, interfaceType)
}
parts = append(parts, sizeLabel)
code := strings.Join(parts, "_")
return code, component.Model
}
func classifyPsULot(component HardwareComponent) (string, *string) {
wattage, ok := floatAttribute(component.Attributes, "wattage_w")
if !ok || wattage <= 0 {
return "", nil
}
vendor := normalizeLotPointer(component.Vendor)
if vendor == "" {
vendor = normalizeLotStringFromAttributes(component.Attributes, "vendor")
}
if vendor == "" {
vendor = "UNKNOWN"
}
code := fmt.Sprintf("PSU_%dW_%s", int(wattage), vendor)
return code, component.Model
}
func classifyPCIELot(component HardwareComponent) (string, *string) {
deviceClass := normalizeLotStringFromAttributes(component.Attributes, "device_class")
if deviceClass == "" {
deviceClass = "PCIE"
}
model := normalizeLotPointer(component.Model)
if model != "" {
code := fmt.Sprintf("PCIE_%s_%s", deviceClass, model)
return code, component.Model
}
vendorID, hasVendorID := intAttribute(component.Attributes, "vendor_id")
deviceID, hasDeviceID := intAttribute(component.Attributes, "device_id")
if hasVendorID && hasDeviceID {
code := fmt.Sprintf("PCIE_%s_%d_%d", deviceClass, vendorID, deviceID)
return code, component.Model
}
return "", nil
}
func normalizeLotPointer(value *string) string {
if value == nil {
return ""
}
return normalizeLotPart(*value)
}
func normalizeLotStringFromAttributes(attrs map[string]any, key string) string {
if attrs == nil {
return ""
}
if v, ok := attrs[key]; ok {
switch typed := v.(type) {
case string:
return normalizeLotPart(typed)
case float64:
return normalizeLotPart(strconv.FormatFloat(typed, 'f', -1, 64))
case int:
return normalizeLotPart(strconv.Itoa(typed))
case int64:
return normalizeLotPart(strconv.FormatInt(int64(typed), 10))
}
}
return ""
}
func floatAttribute(attrs map[string]any, key string) (float64, bool) {
if attrs == nil {
return 0, false
}
if v, ok := attrs[key]; ok {
switch typed := v.(type) {
case float64:
return typed, true
case int:
return float64(typed), true
case int64:
return float64(typed), true
}
}
return 0, false
}
func intAttribute(attrs map[string]any, key string) (int, bool) {
if attrs == nil {
return 0, false
}
if v, ok := attrs[key]; ok {
switch typed := v.(type) {
case float64:
return int(typed), true
case int:
return typed, true
case int64:
return int(typed), true
}
}
return 0, false
}
func formatStorageSize(gb float64) string {
if gb >= 1000 {
tb := gb / 1000
if tb == float64(int64(tb)) {
return fmt.Sprintf("%dTB", int64(tb))
}
return fmt.Sprintf("%.2fTB", tb)
}
if gb == float64(int64(gb)) {
return fmt.Sprintf("%dGB", int64(gb))
}
return fmt.Sprintf("%.2fGB", gb)
}
func normalizeLotPart(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
upper := strings.ToUpper(trimmed)
cleaned := lotNormalizePattern.ReplaceAllString(upper, "_")
cleaned = strings.Trim(cleaned, "_")
cleaned = strings.ReplaceAll(cleaned, "__", "_")
return cleaned
}