Dedup by version caused CPU1 Microcode to be omitted when both CPUs run the same version, leaving the firmware column blank for the second socket. Each CPU gets its own firmware entry keyed by index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1223 lines
33 KiB
Go
1223 lines
33 KiB
Go
package inspur
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
|
)
|
|
|
|
// ParseComponentLog parses component.log file and extracts detailed hardware info
|
|
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|
if hw == nil {
|
|
return
|
|
}
|
|
|
|
text := string(content)
|
|
|
|
// Parse RESTful CPU info — fallback when asset.json is absent
|
|
if len(hw.CPUs) == 0 {
|
|
parseCPUInfo(text, hw)
|
|
}
|
|
|
|
// Parse RESTful Memory info (detailed memory data)
|
|
parseMemoryInfo(text, hw)
|
|
|
|
// Parse RESTful PSU info
|
|
parsePSUInfo(text, hw)
|
|
|
|
// Parse RESTful HDD info
|
|
parseHDDInfo(text, hw)
|
|
|
|
// Parse RESTful diskbackplane info
|
|
parseDiskBackplaneInfo(text, hw)
|
|
|
|
// Parse RESTful Network Adapter info
|
|
parseNetworkAdapterInfo(text, hw)
|
|
|
|
// Extract firmware from all components
|
|
extractComponentFirmware(text, hw)
|
|
}
|
|
|
|
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
|
|
func ParseComponentLogEvents(content []byte) []models.Event {
|
|
var events []models.Event
|
|
text := string(content)
|
|
|
|
// Parse RESTful Memory info for Warning/Error status
|
|
memEvents := parseMemoryEvents(text)
|
|
events = append(events, memEvents...)
|
|
events = append(events, parseFanEvents(text)...)
|
|
|
|
return events
|
|
}
|
|
|
|
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
|
|
func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
|
text := string(content)
|
|
var out []models.SensorReading
|
|
out = append(out, parseFanSensors(text)...)
|
|
out = append(out, parseDiskBackplaneSensors(text)...)
|
|
out = append(out, parsePSUSummarySensors(text)...)
|
|
return out
|
|
}
|
|
|
|
// CPURESTInfo represents the RESTful CPU info structure in component.log
|
|
type CPURESTInfo struct {
|
|
Processors []struct {
|
|
ProcID int `json:"proc_id"`
|
|
CPUID string `json:"PROC_ID"` // uppercase key — prevents case-insensitive collision with proc_id
|
|
Manufacturer string `json:"Manufacturer"`
|
|
MaxSpeedMHz int `json:"MaxSpeedMHz"`
|
|
ConfigStatus int `json:"configStatus"`
|
|
ProcName string `json:"proc_name"`
|
|
ProcStatus int `json:"proc_status"`
|
|
ProcSpeed int `json:"proc_speed"`
|
|
CoreCount int `json:"proc_core_count"`
|
|
ThreadCount int `json:"proc_thread_count"`
|
|
TDP int `json:"proc_tdp"`
|
|
L1Cache int `json:"proc_l1cache_size"`
|
|
L2Cache int `json:"proc_l2cache_size"`
|
|
L3Cache int `json:"proc_l3cache_size"`
|
|
MicroCode string `json:"micro_code"`
|
|
PPIN string `json:"ppin"`
|
|
Status string `json:"status"`
|
|
} `json:"processors"`
|
|
}
|
|
|
|
func parseCPUInfo(text string, hw *models.HardwareConfig) {
|
|
re := regexp.MustCompile(`RESTful CPU info:\s*(\{[\s\S]*?\})\s*RESTful Memory`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var cpuInfo CPURESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &cpuInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
for _, proc := range cpuInfo.Processors {
|
|
if proc.ProcStatus != 1 && proc.ConfigStatus != 1 {
|
|
continue
|
|
}
|
|
hw.CPUs = append(hw.CPUs, models.CPU{
|
|
Socket: proc.ProcID,
|
|
Model: strings.TrimSpace(proc.ProcName),
|
|
Cores: proc.CoreCount,
|
|
Threads: proc.ThreadCount,
|
|
FrequencyMHz: proc.ProcSpeed,
|
|
MaxFreqMHz: proc.MaxSpeedMHz,
|
|
L1CacheKB: proc.L1Cache,
|
|
L2CacheKB: proc.L2Cache,
|
|
L3CacheKB: proc.L3Cache,
|
|
TDP: proc.TDP,
|
|
PPIN: proc.PPIN,
|
|
})
|
|
if proc.MicroCode != "" {
|
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
|
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
|
|
Version: proc.MicroCode,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// MemoryRESTInfo represents the RESTful Memory info structure
|
|
type MemoryRESTInfo struct {
|
|
MemModules []struct {
|
|
MemModID int `json:"mem_mod_id"`
|
|
ConfigStatus int `json:"config_status"`
|
|
MemModSlot string `json:"mem_mod_slot"`
|
|
MemModStatus int `json:"mem_mod_status"`
|
|
MemModSize int `json:"mem_mod_size"`
|
|
MemModType string `json:"mem_mod_type"`
|
|
MemModTechnology string `json:"mem_mod_technology"`
|
|
MemModFrequency int `json:"mem_mod_frequency"`
|
|
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
|
|
MemModVendor string `json:"mem_mod_vendor"`
|
|
MemModPartNum string `json:"mem_mod_part_num"`
|
|
MemModSerial string `json:"mem_mod_serial_num"`
|
|
MemModRanks int `json:"mem_mod_ranks"`
|
|
Status string `json:"status"`
|
|
} `json:"mem_modules"`
|
|
TotalMemoryCount int `json:"total_memory_count"`
|
|
PresentMemoryCount int `json:"present_memory_count"`
|
|
MemTotalMemSize int `json:"mem_total_mem_size"`
|
|
}
|
|
|
|
func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
|
// Find RESTful Memory info section
|
|
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var memInfo MemoryRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
var merged []models.MemoryDIMM
|
|
seen := make(map[string]int)
|
|
for _, existing := range hw.Memory {
|
|
key := inspurMemoryKey(existing)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
seen[key] = len(merged)
|
|
merged = append(merged, existing)
|
|
}
|
|
for _, mem := range memInfo.MemModules {
|
|
item := models.MemoryDIMM{
|
|
Slot: mem.MemModSlot,
|
|
Location: mem.MemModSlot,
|
|
// status=1 with a known serial/part is definitely present even if BMC reports size=0
|
|
Present: mem.MemModStatus == 1 && (mem.MemModSize > 0 || strings.TrimSpace(mem.MemModSerial) != "" || strings.TrimSpace(mem.MemModPartNum) != ""),
|
|
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
|
Type: mem.MemModType,
|
|
Technology: strings.TrimSpace(mem.MemModTechnology),
|
|
MaxSpeedMHz: mem.MemModFrequency,
|
|
CurrentSpeedMHz: mem.MemModCurrentFreq,
|
|
Manufacturer: mem.MemModVendor,
|
|
SerialNumber: mem.MemModSerial,
|
|
PartNumber: strings.TrimSpace(mem.MemModPartNum),
|
|
Status: mem.Status,
|
|
Ranks: mem.MemModRanks,
|
|
}
|
|
key := inspurMemoryKey(item)
|
|
if idx, ok := seen[key]; ok {
|
|
mergeInspurMemoryDIMM(&merged[idx], item)
|
|
continue
|
|
}
|
|
if key != "" {
|
|
seen[key] = len(merged)
|
|
}
|
|
merged = append(merged, item)
|
|
}
|
|
hw.Memory = merged
|
|
}
|
|
|
|
// PSURESTInfo represents the RESTful PSU info structure
|
|
type PSURESTInfo struct {
|
|
PowerSupplies []struct {
|
|
ID int `json:"id"`
|
|
Present int `json:"present"`
|
|
VendorID string `json:"vendor_id"`
|
|
Model string `json:"model"`
|
|
SerialNum string `json:"serial_num"`
|
|
PartNum string `json:"part_num"`
|
|
FwVer string `json:"fw_ver"`
|
|
InputType string `json:"input_type"`
|
|
Status string `json:"status"`
|
|
RatedPower int `json:"rated_power"`
|
|
PSInPower int `json:"ps_in_power"`
|
|
PSOutPower int `json:"ps_out_power"`
|
|
PSInVolt float64 `json:"ps_in_volt"`
|
|
PSOutVolt float64 `json:"ps_out_volt"`
|
|
PSUMaxTemp int `json:"psu_max_temperature"`
|
|
} `json:"power_supplies"`
|
|
PresentPowerReading int `json:"present_power_reading"`
|
|
}
|
|
|
|
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
|
// Find RESTful PSU info section
|
|
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var psuInfo PSURESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
var merged []models.PSU
|
|
seen := make(map[string]int)
|
|
for _, existing := range hw.PowerSupply {
|
|
key := inspurPSUKey(existing)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
seen[key] = len(merged)
|
|
merged = append(merged, existing)
|
|
}
|
|
for _, psu := range psuInfo.PowerSupplies {
|
|
item := models.PSU{
|
|
Slot: fmt.Sprintf("PSU%d", psu.ID),
|
|
Present: psu.Present == 1,
|
|
Model: strings.TrimSpace(psu.Model),
|
|
Vendor: strings.TrimSpace(psu.VendorID),
|
|
WattageW: psu.RatedPower,
|
|
SerialNumber: strings.TrimSpace(psu.SerialNum),
|
|
PartNumber: strings.TrimSpace(psu.PartNum),
|
|
Firmware: psu.FwVer,
|
|
Status: psu.Status,
|
|
InputType: psu.InputType,
|
|
InputPowerW: psu.PSInPower,
|
|
OutputPowerW: psu.PSOutPower,
|
|
InputVoltage: psu.PSInVolt,
|
|
OutputVoltage: psu.PSOutVolt,
|
|
TemperatureC: psu.PSUMaxTemp,
|
|
}
|
|
key := inspurPSUKey(item)
|
|
if idx, ok := seen[key]; ok {
|
|
mergeInspurPSU(&merged[idx], item)
|
|
continue
|
|
}
|
|
if key != "" {
|
|
seen[key] = len(merged)
|
|
}
|
|
merged = append(merged, item)
|
|
}
|
|
hw.PowerSupply = merged
|
|
}
|
|
|
|
// HDDRESTInfo represents the RESTful HDD info structure
|
|
type HDDRESTInfo []struct {
|
|
ID int `json:"id"`
|
|
Present int `json:"present"`
|
|
Enable int `json:"enable"`
|
|
SN string `json:"SN"`
|
|
Model string `json:"model"`
|
|
Capacity int `json:"capacity"`
|
|
Manufacture string `json:"manufacture"`
|
|
Firmware string `json:"firmware"`
|
|
LocationString string `json:"locationstring"`
|
|
CapableSpeed int `json:"capablespeed"`
|
|
}
|
|
|
|
func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
|
// Find RESTful HDD info section
|
|
re := regexp.MustCompile(`RESTful HDD info:\s*(\[[\s\S]*?\])\s*RESTful PSU`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var hddInfo HDDRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &hddInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
// Update storage with detailed info (merge with existing data from asset.json)
|
|
hddMap := make(map[string]struct {
|
|
SN string
|
|
Model string
|
|
Firmware string
|
|
Mfr string
|
|
})
|
|
for _, hdd := range hddInfo {
|
|
if hdd.Present == 1 {
|
|
slot := strings.TrimSpace(hdd.LocationString)
|
|
if slot == "" {
|
|
slot = fmt.Sprintf("HDD%d", hdd.ID)
|
|
}
|
|
hddMap[slot] = struct {
|
|
SN string
|
|
Model string
|
|
Firmware string
|
|
Mfr string
|
|
}{
|
|
SN: normalizeRedisValue(hdd.SN),
|
|
Model: strings.TrimSpace(hdd.Model),
|
|
Firmware: normalizeRedisValue(hdd.Firmware),
|
|
Mfr: strings.TrimSpace(hdd.Manufacture),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge into existing inventory first (asset/other sections).
|
|
for i := range hw.Storage {
|
|
slot := strings.TrimSpace(hw.Storage[i].Slot)
|
|
if slot == "" {
|
|
continue
|
|
}
|
|
detail, ok := hddMap[slot]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if normalizeRedisValue(hw.Storage[i].SerialNumber) == "" {
|
|
hw.Storage[i].SerialNumber = detail.SN
|
|
}
|
|
if hw.Storage[i].Model == "" {
|
|
hw.Storage[i].Model = detail.Model
|
|
}
|
|
if normalizeRedisValue(hw.Storage[i].Firmware) == "" {
|
|
hw.Storage[i].Firmware = detail.Firmware
|
|
}
|
|
if hw.Storage[i].Manufacturer == "" {
|
|
hw.Storage[i].Manufacturer = detail.Mfr
|
|
}
|
|
hw.Storage[i].Present = true
|
|
}
|
|
|
|
// If storage is empty, populate from HDD info
|
|
if len(hw.Storage) == 0 {
|
|
for _, hdd := range hddInfo {
|
|
if hdd.Present != 1 {
|
|
continue
|
|
}
|
|
storType := "HDD"
|
|
model := strings.TrimSpace(hdd.Model)
|
|
if strings.Contains(strings.ToUpper(model), "SSD") || strings.Contains(model, "MZ7") {
|
|
storType = "SSD"
|
|
}
|
|
|
|
iface := "SATA"
|
|
if hdd.CapableSpeed == 12 {
|
|
iface = "SAS"
|
|
}
|
|
slot := strings.TrimSpace(hdd.LocationString)
|
|
if slot == "" {
|
|
slot = fmt.Sprintf("HDD%d", hdd.ID)
|
|
}
|
|
|
|
hw.Storage = append(hw.Storage, models.Storage{
|
|
Slot: slot,
|
|
Type: storType,
|
|
Model: model,
|
|
SizeGB: hdd.Capacity,
|
|
SerialNumber: normalizeRedisValue(hdd.SN),
|
|
Manufacturer: extractStorageManufacturer(model),
|
|
Firmware: normalizeRedisValue(hdd.Firmware),
|
|
Interface: iface,
|
|
Present: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// FanRESTInfo represents the RESTful fan info structure.
|
|
type FanRESTInfo struct {
|
|
Fans []struct {
|
|
ID int `json:"id"`
|
|
FanName string `json:"fan_name"`
|
|
Present string `json:"present"`
|
|
Status string `json:"status"`
|
|
StatusStr string `json:"status_str"`
|
|
SpeedRPM int `json:"speed_rpm"`
|
|
SpeedPercent int `json:"speed_percent"`
|
|
MaxSpeedRPM int `json:"max_speed_rpm"`
|
|
FanModel string `json:"fan_model"`
|
|
} `json:"fans"`
|
|
FansPower int `json:"fans_power"`
|
|
}
|
|
|
|
// NetworkAdapterRESTInfo represents the RESTful Network Adapter info structure
|
|
type NetworkAdapterRESTInfo struct {
|
|
SysAdapters []struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Location string `json:"Location"`
|
|
Present int `json:"present"`
|
|
Slot int `json:"slot"`
|
|
VendorID int `json:"vendor_id"`
|
|
DeviceID int `json:"device_id"`
|
|
Vendor string `json:"vendor"`
|
|
Model string `json:"model"`
|
|
FwVer string `json:"fw_ver"`
|
|
Status string `json:"status"`
|
|
SN string `json:"sn"`
|
|
PN string `json:"pn"`
|
|
PortNum int `json:"port_num"`
|
|
PortType string `json:"port_type"`
|
|
Ports []struct {
|
|
ID int `json:"id"`
|
|
MacAddr string `json:"mac_addr"`
|
|
} `json:"ports"`
|
|
} `json:"sys_adapters"`
|
|
}
|
|
|
|
func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
|
// Find RESTful Network Adapter info section
|
|
re := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var netInfo NetworkAdapterRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
var merged []models.NetworkAdapter
|
|
seen := make(map[string]int)
|
|
for _, existing := range hw.NetworkAdapters {
|
|
key := inspurNICKey(existing)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
seen[key] = len(merged)
|
|
merged = append(merged, existing)
|
|
}
|
|
for _, adapter := range netInfo.SysAdapters {
|
|
var macs []string
|
|
for _, port := range adapter.Ports {
|
|
if port.MacAddr != "" {
|
|
macs = append(macs, port.MacAddr)
|
|
}
|
|
}
|
|
|
|
model := normalizeModelLabel(adapter.Model)
|
|
if model == "" || looksLikeRawDeviceID(model) {
|
|
if resolved := normalizeModelLabel(pciids.DeviceName(adapter.VendorID, adapter.DeviceID)); resolved != "" {
|
|
model = resolved
|
|
}
|
|
}
|
|
vendor := normalizeModelLabel(adapter.Vendor)
|
|
if vendor == "" {
|
|
vendor = normalizeModelLabel(pciids.VendorName(adapter.VendorID))
|
|
}
|
|
|
|
item := models.NetworkAdapter{
|
|
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
|
|
Location: adapter.Location,
|
|
Present: adapter.Present == 1,
|
|
Model: model,
|
|
Vendor: vendor,
|
|
VendorID: adapter.VendorID,
|
|
DeviceID: adapter.DeviceID,
|
|
SerialNumber: normalizeRedisValue(adapter.SN),
|
|
PartNumber: normalizeRedisValue(adapter.PN),
|
|
Firmware: normalizeRedisValue(adapter.FwVer),
|
|
PortCount: adapter.PortNum,
|
|
PortType: adapter.PortType,
|
|
MACAddresses: macs,
|
|
Status: adapter.Status,
|
|
}
|
|
key := inspurNICKey(item)
|
|
if idx, ok := seen[key]; ok {
|
|
mergeInspurNIC(&merged[idx], item)
|
|
continue
|
|
}
|
|
if slotIdx := inspurFindNICBySlot(merged, item.Slot); slotIdx >= 0 {
|
|
mergeInspurNIC(&merged[slotIdx], item)
|
|
if key != "" {
|
|
seen[key] = slotIdx
|
|
}
|
|
continue
|
|
}
|
|
if key != "" {
|
|
seen[key] = len(merged)
|
|
}
|
|
merged = append(merged, item)
|
|
}
|
|
hw.NetworkAdapters = merged
|
|
}
|
|
|
|
func inspurMemoryKey(item models.MemoryDIMM) string {
|
|
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, item.Slot, item.Location)))
|
|
}
|
|
|
|
func mergeInspurMemoryDIMM(dst *models.MemoryDIMM, src models.MemoryDIMM) {
|
|
if dst == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(dst.Slot) == "" {
|
|
dst.Slot = src.Slot
|
|
}
|
|
if strings.TrimSpace(dst.Location) == "" {
|
|
dst.Location = src.Location
|
|
}
|
|
dst.Present = dst.Present || src.Present
|
|
if dst.SizeMB == 0 {
|
|
dst.SizeMB = src.SizeMB
|
|
}
|
|
if strings.TrimSpace(dst.Type) == "" {
|
|
dst.Type = src.Type
|
|
}
|
|
if strings.TrimSpace(dst.Technology) == "" {
|
|
dst.Technology = src.Technology
|
|
}
|
|
if dst.MaxSpeedMHz == 0 {
|
|
dst.MaxSpeedMHz = src.MaxSpeedMHz
|
|
}
|
|
if dst.CurrentSpeedMHz == 0 {
|
|
dst.CurrentSpeedMHz = src.CurrentSpeedMHz
|
|
}
|
|
if strings.TrimSpace(dst.Manufacturer) == "" {
|
|
dst.Manufacturer = src.Manufacturer
|
|
}
|
|
if strings.TrimSpace(dst.SerialNumber) == "" {
|
|
dst.SerialNumber = src.SerialNumber
|
|
}
|
|
if strings.TrimSpace(dst.PartNumber) == "" {
|
|
dst.PartNumber = src.PartNumber
|
|
}
|
|
if strings.TrimSpace(dst.Status) == "" {
|
|
dst.Status = src.Status
|
|
}
|
|
if dst.Ranks == 0 {
|
|
dst.Ranks = src.Ranks
|
|
}
|
|
}
|
|
|
|
func inspurPSUKey(item models.PSU) string {
|
|
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, item.Slot, item.Model)))
|
|
}
|
|
|
|
func mergeInspurPSU(dst *models.PSU, src models.PSU) {
|
|
if dst == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(dst.Slot) == "" {
|
|
dst.Slot = src.Slot
|
|
}
|
|
dst.Present = dst.Present || src.Present
|
|
if strings.TrimSpace(dst.Model) == "" {
|
|
dst.Model = src.Model
|
|
}
|
|
if strings.TrimSpace(dst.Vendor) == "" {
|
|
dst.Vendor = src.Vendor
|
|
}
|
|
if dst.WattageW == 0 {
|
|
dst.WattageW = src.WattageW
|
|
}
|
|
if strings.TrimSpace(dst.SerialNumber) == "" {
|
|
dst.SerialNumber = src.SerialNumber
|
|
}
|
|
if strings.TrimSpace(dst.PartNumber) == "" {
|
|
dst.PartNumber = src.PartNumber
|
|
}
|
|
if strings.TrimSpace(dst.Firmware) == "" {
|
|
dst.Firmware = src.Firmware
|
|
}
|
|
if strings.TrimSpace(dst.Status) == "" {
|
|
dst.Status = src.Status
|
|
}
|
|
if strings.TrimSpace(dst.InputType) == "" {
|
|
dst.InputType = src.InputType
|
|
}
|
|
if dst.InputPowerW == 0 {
|
|
dst.InputPowerW = src.InputPowerW
|
|
}
|
|
if dst.OutputPowerW == 0 {
|
|
dst.OutputPowerW = src.OutputPowerW
|
|
}
|
|
if dst.InputVoltage == 0 {
|
|
dst.InputVoltage = src.InputVoltage
|
|
}
|
|
if dst.OutputVoltage == 0 {
|
|
dst.OutputVoltage = src.OutputVoltage
|
|
}
|
|
if dst.TemperatureC == 0 {
|
|
dst.TemperatureC = src.TemperatureC
|
|
}
|
|
}
|
|
|
|
func inspurNICKey(item models.NetworkAdapter) string {
|
|
return strings.ToLower(strings.TrimSpace(inspurFirstNonEmpty(item.SerialNumber, strings.Join(item.MACAddresses, ","), item.Slot, item.Location)))
|
|
}
|
|
|
|
func mergeInspurNIC(dst *models.NetworkAdapter, src models.NetworkAdapter) {
|
|
if dst == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(dst.Slot) == "" {
|
|
dst.Slot = src.Slot
|
|
}
|
|
if strings.TrimSpace(dst.Location) == "" {
|
|
dst.Location = src.Location
|
|
}
|
|
dst.Present = dst.Present || src.Present
|
|
if strings.TrimSpace(dst.BDF) == "" {
|
|
dst.BDF = src.BDF
|
|
}
|
|
if strings.TrimSpace(dst.Model) == "" {
|
|
dst.Model = src.Model
|
|
}
|
|
if strings.TrimSpace(dst.Description) == "" {
|
|
dst.Description = src.Description
|
|
}
|
|
if strings.TrimSpace(dst.Vendor) == "" {
|
|
dst.Vendor = src.Vendor
|
|
}
|
|
if dst.VendorID == 0 {
|
|
dst.VendorID = src.VendorID
|
|
}
|
|
if dst.DeviceID == 0 {
|
|
dst.DeviceID = src.DeviceID
|
|
}
|
|
if strings.TrimSpace(dst.SerialNumber) == "" {
|
|
dst.SerialNumber = src.SerialNumber
|
|
}
|
|
if strings.TrimSpace(dst.PartNumber) == "" {
|
|
dst.PartNumber = src.PartNumber
|
|
}
|
|
if strings.TrimSpace(dst.Firmware) == "" {
|
|
dst.Firmware = src.Firmware
|
|
}
|
|
if dst.PortCount == 0 {
|
|
dst.PortCount = src.PortCount
|
|
}
|
|
if strings.TrimSpace(dst.PortType) == "" {
|
|
dst.PortType = src.PortType
|
|
}
|
|
if dst.LinkWidth == 0 {
|
|
dst.LinkWidth = src.LinkWidth
|
|
}
|
|
if strings.TrimSpace(dst.LinkSpeed) == "" {
|
|
dst.LinkSpeed = src.LinkSpeed
|
|
}
|
|
if dst.MaxLinkWidth == 0 {
|
|
dst.MaxLinkWidth = src.MaxLinkWidth
|
|
}
|
|
if strings.TrimSpace(dst.MaxLinkSpeed) == "" {
|
|
dst.MaxLinkSpeed = src.MaxLinkSpeed
|
|
}
|
|
if dst.NUMANode == 0 {
|
|
dst.NUMANode = src.NUMANode
|
|
}
|
|
if strings.TrimSpace(dst.Status) == "" {
|
|
dst.Status = src.Status
|
|
}
|
|
for _, mac := range src.MACAddresses {
|
|
mac = strings.TrimSpace(mac)
|
|
if mac == "" {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, existing := range dst.MACAddresses {
|
|
if strings.EqualFold(strings.TrimSpace(existing), mac) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
dst.MACAddresses = append(dst.MACAddresses, mac)
|
|
}
|
|
}
|
|
}
|
|
|
|
func inspurFindNICBySlot(items []models.NetworkAdapter, slot string) int {
|
|
slot = strings.ToLower(strings.TrimSpace(slot))
|
|
if slot == "" {
|
|
return -1
|
|
}
|
|
for i := range items {
|
|
if strings.ToLower(strings.TrimSpace(items[i].Slot)) == slot {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func inspurFirstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseFanSensors(text string) []models.SensorReading {
|
|
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return nil
|
|
}
|
|
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var fanInfo FanRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
|
|
return nil
|
|
}
|
|
|
|
out := make([]models.SensorReading, 0, len(fanInfo.Fans)+1)
|
|
for _, fan := range fanInfo.Fans {
|
|
name := strings.TrimSpace(fan.FanName)
|
|
if name == "" {
|
|
name = fmt.Sprintf("FAN%d", fan.ID)
|
|
}
|
|
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
|
|
raw := fmt.Sprintf("rpm=%d pct=%d model=%s max_rpm=%d", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel, fan.MaxSpeedRPM)
|
|
out = append(out, models.SensorReading{
|
|
Name: name,
|
|
Type: "fan_speed",
|
|
Value: float64(fan.SpeedRPM),
|
|
Unit: "RPM",
|
|
RawValue: raw,
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
if fanInfo.FansPower > 0 {
|
|
out = append(out, models.SensorReading{
|
|
Name: "Fans_Power",
|
|
Type: "power",
|
|
Value: float64(fanInfo.FansPower),
|
|
Unit: "W",
|
|
RawValue: fmt.Sprintf("%d", fanInfo.FansPower),
|
|
Status: "OK",
|
|
})
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func parseFanEvents(text string) []models.Event {
|
|
re := regexp.MustCompile(`RESTful fan info:\s*(\{[\s\S]*?\})\s*RESTful diskbackplane`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return nil
|
|
}
|
|
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var fanInfo FanRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &fanInfo); err != nil {
|
|
return nil
|
|
}
|
|
|
|
var events []models.Event
|
|
for _, fan := range fanInfo.Fans {
|
|
status := normalizeComponentStatus(fan.StatusStr, fan.Status, fan.Present)
|
|
if isHealthyComponentStatus(status) {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSpace(fan.FanName)
|
|
if name == "" {
|
|
name = fmt.Sprintf("FAN%d", fan.ID)
|
|
}
|
|
|
|
severity := models.SeverityWarning
|
|
lowStatus := strings.ToLower(status)
|
|
if strings.Contains(lowStatus, "critical") || strings.Contains(lowStatus, "fail") || strings.Contains(lowStatus, "error") {
|
|
severity = models.SeverityCritical
|
|
}
|
|
|
|
events = append(events, models.Event{
|
|
ID: fmt.Sprintf("fan_%d_status", fan.ID),
|
|
Timestamp: time.Now(),
|
|
Source: "Fan",
|
|
SensorType: "fan",
|
|
SensorName: name,
|
|
EventType: "Fan Status",
|
|
Severity: severity,
|
|
Description: fmt.Sprintf("%s reports %s", name, status),
|
|
RawData: fmt.Sprintf("rpm=%d pct=%d model=%s", fan.SpeedRPM, fan.SpeedPercent, fan.FanModel),
|
|
})
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
func parseDiskBackplaneSensors(text string) []models.SensorReading {
|
|
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return nil
|
|
}
|
|
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var backplaneInfo DiskBackplaneRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
|
|
return nil
|
|
}
|
|
|
|
out := make([]models.SensorReading, 0, len(backplaneInfo))
|
|
for _, bp := range backplaneInfo {
|
|
if bp.Present != 1 {
|
|
continue
|
|
}
|
|
name := fmt.Sprintf("Backplane%d_Temp", bp.BackplaneIndex)
|
|
status := "OK"
|
|
if bp.Temperature <= 0 {
|
|
status = "unknown"
|
|
}
|
|
raw := fmt.Sprintf("front=%d ports=%d drives=%d cpld=%s", bp.Front, bp.PortCount, bp.DriverCount, bp.CPLDVersion)
|
|
out = append(out, models.SensorReading{
|
|
Name: name,
|
|
Type: "temperature",
|
|
Value: float64(bp.Temperature),
|
|
Unit: "C",
|
|
RawValue: raw,
|
|
Status: status,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parsePSUSummarySensors(text string) []models.SensorReading {
|
|
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return nil
|
|
}
|
|
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var psuInfo PSURESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
|
|
return nil
|
|
}
|
|
|
|
out := make([]models.SensorReading, 0, len(psuInfo.PowerSupplies)*3+1)
|
|
if psuInfo.PresentPowerReading > 0 {
|
|
out = append(out, models.SensorReading{
|
|
Name: "PSU_Present_Power_Reading",
|
|
Type: "power",
|
|
Value: float64(psuInfo.PresentPowerReading),
|
|
Unit: "W",
|
|
RawValue: fmt.Sprintf("%d", psuInfo.PresentPowerReading),
|
|
Status: "OK",
|
|
})
|
|
}
|
|
|
|
for _, psu := range psuInfo.PowerSupplies {
|
|
if psu.Present != 1 {
|
|
continue
|
|
}
|
|
status := normalizeComponentStatus(psu.Status)
|
|
out = append(out, models.SensorReading{
|
|
Name: fmt.Sprintf("PSU%d_InputPower", psu.ID),
|
|
Type: "power",
|
|
Value: float64(psu.PSInPower),
|
|
Unit: "W",
|
|
RawValue: fmt.Sprintf("%d", psu.PSInPower),
|
|
Status: status,
|
|
})
|
|
out = append(out, models.SensorReading{
|
|
Name: fmt.Sprintf("PSU%d_OutputPower", psu.ID),
|
|
Type: "power",
|
|
Value: float64(psu.PSOutPower),
|
|
Unit: "W",
|
|
RawValue: fmt.Sprintf("%d", psu.PSOutPower),
|
|
Status: status,
|
|
})
|
|
out = append(out, models.SensorReading{
|
|
Name: fmt.Sprintf("PSU%d_Temp", psu.ID),
|
|
Type: "temperature",
|
|
Value: float64(psu.PSUMaxTemp),
|
|
Unit: "C",
|
|
RawValue: fmt.Sprintf("%d", psu.PSUMaxTemp),
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func normalizeComponentStatus(values ...string) string {
|
|
for _, v := range values {
|
|
s := strings.TrimSpace(v)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
return s
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func isHealthyComponentStatus(status string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "", "ok", "normal", "present", "enabled":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var rawDeviceIDLikeRegex = regexp.MustCompile(`(?i)^(?:0x)?[0-9a-f]{3,4}$`)
|
|
|
|
func looksLikeRawDeviceID(v string) bool {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return true
|
|
}
|
|
return rawDeviceIDLikeRegex.MatchString(v)
|
|
}
|
|
|
|
func parseMemoryEvents(text string) []models.Event {
|
|
var events []models.Event
|
|
|
|
// Find RESTful Memory info section
|
|
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return events
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var memInfo MemoryRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
|
return events
|
|
}
|
|
|
|
// Generate events for memory modules with Warning or Error status
|
|
for _, mem := range memInfo.MemModules {
|
|
if mem.Status == "Warning" || mem.Status == "Error" || mem.Status == "Critical" {
|
|
severity := models.SeverityWarning
|
|
if mem.Status == "Error" || mem.Status == "Critical" {
|
|
severity = models.SeverityCritical
|
|
}
|
|
|
|
description := fmt.Sprintf("Memory module %s: %s", mem.MemModSlot, mem.Status)
|
|
if mem.MemModSize == 0 {
|
|
description = fmt.Sprintf("Memory module %s not detected (capacity 0GB)", mem.MemModSlot)
|
|
}
|
|
|
|
events = append(events, models.Event{
|
|
ID: fmt.Sprintf("mem_%d", mem.MemModID),
|
|
Timestamp: time.Now(),
|
|
Source: "Memory",
|
|
SensorType: "memory",
|
|
SensorName: mem.MemModSlot,
|
|
EventType: "Memory Status",
|
|
Severity: severity,
|
|
Description: description,
|
|
RawData: fmt.Sprintf("Slot: %s, Vendor: %s, P/N: %s, S/N: %s", mem.MemModSlot, mem.MemModVendor, mem.MemModPartNum, mem.MemModSerial),
|
|
})
|
|
}
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
// extractComponentFirmware extracts firmware versions from all component data
|
|
func extractComponentFirmware(text string, hw *models.HardwareConfig) {
|
|
// Create a map to track existing firmware entries (avoid duplicates)
|
|
existingFW := make(map[string]bool)
|
|
for _, fw := range hw.Firmware {
|
|
existingFW[fw.DeviceName] = true
|
|
}
|
|
|
|
// HDD firmware is already extracted from asset.json with better names
|
|
// Skip extracting from component.log to avoid duplicates
|
|
|
|
// Extract PSU firmware from RESTful PSU info
|
|
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
|
if match := rePSU.FindStringSubmatch(text); match != nil {
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var psuInfo PSURESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err == nil {
|
|
for _, psu := range psuInfo.PowerSupplies {
|
|
if psu.Present == 1 && psu.FwVer != "" {
|
|
fwName := fmt.Sprintf("PSU%d (%s)", psu.ID, psu.Model)
|
|
if !existingFW[fwName] {
|
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
|
DeviceName: fwName,
|
|
Version: psu.FwVer,
|
|
})
|
|
existingFW[fwName] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract Network Adapter firmware from RESTful Network Adapter info
|
|
reNet := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
|
|
if match := reNet.FindStringSubmatch(text); match != nil {
|
|
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
|
var netInfo NetworkAdapterRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err == nil {
|
|
for _, adapter := range netInfo.SysAdapters {
|
|
if adapter.Present == 1 && adapter.FwVer != "" && adapter.FwVer != "NA" {
|
|
fwName := fmt.Sprintf("NIC %s (%s)", adapter.Location, adapter.Model)
|
|
if !existingFW[fwName] {
|
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
|
DeviceName: fwName,
|
|
Version: adapter.FwVer,
|
|
})
|
|
existingFW[fwName] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract BMC, CPLD and VR firmware from RESTful version info section.
|
|
// The JSON is a flat array: [{"id":N,"dev_name":"...","dev_version":"..."}, ...]
|
|
reVer := regexp.MustCompile(`RESTful version info:\s*(\[[\s\S]*?\])\s*RESTful`)
|
|
if match := reVer.FindStringSubmatch(text); match != nil {
|
|
type verEntry struct {
|
|
DevName string `json:"dev_name"`
|
|
DevVersion string `json:"dev_version"`
|
|
}
|
|
var entries []verEntry
|
|
if err := json.Unmarshal([]byte(match[1]), &entries); err == nil {
|
|
for _, e := range entries {
|
|
name := normalizeVersionInfoName(e.DevName)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
version := strings.TrimSpace(e.DevVersion)
|
|
if version == "" {
|
|
continue
|
|
}
|
|
if existingFW[name] {
|
|
continue
|
|
}
|
|
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
|
DeviceName: name,
|
|
Version: version,
|
|
})
|
|
existingFW[name] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// normalizeVersionInfoName converts RESTful version info dev_name to a clean label.
|
|
// Returns "" for entries that should be skipped (inactive BMC, PSU slots).
|
|
func normalizeVersionInfoName(name string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
// Skip PSU_N entries — firmware already extracted from PSU info section.
|
|
if regexp.MustCompile(`(?i)^PSU_\d+$`).MatchString(name) {
|
|
return ""
|
|
}
|
|
// Skip the inactive BMC partition.
|
|
if strings.HasPrefix(strings.ToLower(name), "inactivate(") {
|
|
return ""
|
|
}
|
|
// Active BMC: "Activate(BMC1)" → "BMC"
|
|
if strings.HasPrefix(strings.ToLower(name), "activate(") {
|
|
return "BMC"
|
|
}
|
|
// Strip trailing "Version" suffix (case-insensitive), e.g. "MainBoard0CPLDVersion" → "MainBoard0CPLD"
|
|
if strings.HasSuffix(strings.ToLower(name), "version") {
|
|
name = name[:len(name)-len("version")]
|
|
}
|
|
return strings.TrimSpace(name)
|
|
}
|
|
|
|
// DiskBackplaneRESTInfo represents the RESTful diskbackplane info structure
|
|
type DiskBackplaneRESTInfo []struct {
|
|
PortCount int `json:"port_count"`
|
|
DriverCount int `json:"driver_count"`
|
|
Front int `json:"front"`
|
|
BackplaneIndex int `json:"backplane_index"`
|
|
Present int `json:"present"`
|
|
CPLDVersion string `json:"cpld_version"`
|
|
Temperature int `json:"temperature"`
|
|
}
|
|
|
|
func parseDiskBackplaneInfo(text string, hw *models.HardwareConfig) {
|
|
// Find RESTful diskbackplane info section
|
|
re := regexp.MustCompile(`RESTful diskbackplane info:\s*(\[[\s\S]*?\])\s*BMC`)
|
|
match := re.FindStringSubmatch(text)
|
|
if match == nil {
|
|
return
|
|
}
|
|
|
|
jsonStr := match[1]
|
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
|
|
|
var backplaneInfo DiskBackplaneRESTInfo
|
|
if err := json.Unmarshal([]byte(jsonStr), &backplaneInfo); err != nil {
|
|
return
|
|
}
|
|
|
|
presentByBackplane := make(map[int]int)
|
|
totalPresent := 0
|
|
for _, bp := range backplaneInfo {
|
|
if bp.Present != 1 {
|
|
continue
|
|
}
|
|
if bp.DriverCount <= 0 {
|
|
continue
|
|
}
|
|
limit := bp.DriverCount
|
|
if bp.PortCount > 0 && limit > bp.PortCount {
|
|
limit = bp.PortCount
|
|
}
|
|
presentByBackplane[bp.BackplaneIndex] = limit
|
|
totalPresent += limit
|
|
}
|
|
|
|
if totalPresent == 0 {
|
|
return
|
|
}
|
|
|
|
existingPresent := countPresentStorage(hw.Storage)
|
|
remaining := totalPresent - existingPresent
|
|
if remaining <= 0 {
|
|
return
|
|
}
|
|
|
|
for _, bp := range backplaneInfo {
|
|
if bp.Present != 1 || remaining <= 0 {
|
|
continue
|
|
}
|
|
driveCount := presentByBackplane[bp.BackplaneIndex]
|
|
if driveCount <= 0 {
|
|
continue
|
|
}
|
|
|
|
location := "Rear"
|
|
if bp.Front == 1 {
|
|
location = "Front"
|
|
}
|
|
|
|
for i := 0; i < driveCount && remaining > 0; i++ {
|
|
slot := fmt.Sprintf("BP%d:%d", bp.BackplaneIndex, i)
|
|
if hasStorageSlot(hw.Storage, slot) {
|
|
continue
|
|
}
|
|
|
|
hw.Storage = append(hw.Storage, models.Storage{
|
|
Slot: slot,
|
|
Present: true,
|
|
Location: location,
|
|
BackplaneID: bp.BackplaneIndex,
|
|
Type: "HDD",
|
|
})
|
|
remaining--
|
|
}
|
|
}
|
|
}
|
|
|
|
func countPresentStorage(storage []models.Storage) int {
|
|
count := 0
|
|
for _, dev := range storage {
|
|
if dev.Present {
|
|
count++
|
|
continue
|
|
}
|
|
if strings.TrimSpace(dev.Slot) != "" && (normalizeRedisValue(dev.Model) != "" || normalizeRedisValue(dev.SerialNumber) != "" || dev.SizeGB > 0) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func hasStorageSlot(storage []models.Storage, slot string) bool {
|
|
slot = strings.ToLower(strings.TrimSpace(slot))
|
|
if slot == "" {
|
|
return false
|
|
}
|
|
for _, dev := range storage {
|
|
if strings.ToLower(strings.TrimSpace(dev.Slot)) == slot {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|