264 lines
6.0 KiB
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
|
|
}
|