1155 lines
31 KiB
Go
1155 lines
31 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 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
|
|
}
|
|
|
|
// 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,
|
|
Present: mem.MemModStatus == 1 && mem.MemModSize > 0,
|
|
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
|
|
}
|