Files
logpile/internal/parser/vendors/xfusion/hardware.go
2026-04-04 15:07:10 +03:00

1082 lines
30 KiB
Go

package xfusion
import (
"fmt"
"strconv"
"strings"
"time"
"git.mchus.pro/mchus/logpile/internal/models"
"git.mchus.pro/mchus/logpile/internal/parser"
)
type xfusionNICCard struct {
Slot string
Model string
ProductName string
Vendor string
VendorID int
DeviceID int
BDF string
SerialNumber string
PartNumber string
}
type xfusionNetcardPort struct {
BDF string
MAC string
ActualMAC string
}
type xfusionNetcardSnapshot struct {
Timestamp time.Time
Slot string
ProductName string
Manufacturer string
Firmware string
Ports []xfusionNetcardPort
}
// ── FRU ──────────────────────────────────────────────────────────────────────
// parseFRUInfo parses fruinfo.txt and populates result.FRU and result.Hardware.BoardInfo.
// The file contains IPMI FRU blocks separated by "FRU Device Description" header lines.
func parseFRUInfo(content []byte, result *models.AnalysisResult) {
type fruBlock struct {
header string
fields map[string]string
}
var blocks []fruBlock
var current *fruBlock
for _, line := range strings.Split(string(content), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "FRU Device Description") {
if current != nil {
blocks = append(blocks, *current)
}
current = &fruBlock{header: trimmed, fields: make(map[string]string)}
continue
}
if current == nil {
continue
}
idx := strings.Index(trimmed, " : ")
if idx < 0 {
continue
}
key := strings.TrimSpace(trimmed[:idx])
val := strings.TrimSpace(trimmed[idx+3:])
if key != "" && current.fields[key] == "" {
current.fields[key] = val
}
}
if current != nil {
blocks = append(blocks, *current)
}
for _, b := range blocks {
f := b.fields
fru := models.FRUInfo{
Description: extractFRUHeaderDesc(b.header),
Manufacturer: firstNonEmpty(f["Board Manufacturer"], f["Product Manufacturer"]),
ProductName: f["Product Name"],
SerialNumber: firstNonEmpty(f["Product Serial Number"], f["Board Serial Number"]),
PartNumber: firstNonEmpty(f["Product Part Number"], f["Board Part Number"]),
MfgDate: f["Board Mfg. Date"],
}
if fru.Description != "" || fru.ProductName != "" || fru.SerialNumber != "" {
result.FRU = append(result.FRU, fru)
}
}
// Set BoardInfo from the mainboard block (ID 0).
for _, b := range blocks {
hdr := strings.ToLower(b.header)
if strings.Contains(hdr, "id 0") || strings.Contains(hdr, "mainboard") {
f := b.fields
result.Hardware.BoardInfo = models.BoardInfo{
Manufacturer: firstNonEmpty(f["Product Manufacturer"], f["Board Manufacturer"]),
ProductName: firstNonEmpty(f["Product Name"], f["Board Product Name"]),
SerialNumber: firstNonEmpty(f["Product Serial Number"], f["Board Serial Number"]),
PartNumber: firstNonEmpty(f["Product Part Number"], f["Board Part Number"]),
}
break
}
}
}
func extractFRUHeaderDesc(header string) string {
// "FRU Device Description : Builtin FRU Device (ID 0, Mainboard)"
idx := strings.Index(header, " : ")
if idx >= 0 {
return strings.TrimSpace(header[idx+3:])
}
return header
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
v = strings.TrimSpace(v)
if v != "" {
return v
}
}
return ""
}
// ── Sensors ───────────────────────────────────────────────────────────────────
// parseSensorInfo parses the pipe-delimited IPMI sensor table from sensor_info.txt.
// Columns: sensor id | sensor name | value | unit | status | thresholds...
func parseSensorInfo(content []byte) []models.SensorReading {
var sensors []models.SensorReading
inTable := false
for _, line := range strings.Split(string(content), "\n") {
if strings.Contains(line, "sensor id") && strings.Contains(line, "sensor name") {
inTable = true
continue
}
if inTable && strings.HasPrefix(strings.TrimSpace(line), "**") {
// "*** Detailed Voltage Object Information ***" signals end of main table
inTable = false
continue
}
if !inTable || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 5 {
continue
}
name := strings.TrimSpace(parts[1])
valueStr := strings.TrimSpace(parts[2])
unit := strings.TrimSpace(parts[3])
status := strings.TrimSpace(parts[4])
if name == "" || valueStr == "na" || unit == "discrete" || unit == "unspecified" {
continue
}
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
continue
}
sensors = append(sensors, models.SensorReading{
Name: name,
Type: sensorType(name, unit),
Value: value,
Unit: mapSensorUnit(unit),
RawValue: valueStr,
Status: status,
})
}
return sensors
}
func mapSensorUnit(u string) string {
switch strings.ToLower(strings.TrimSpace(u)) {
case "degrees c":
return "C"
case "volts":
return "V"
case "watts":
return "W"
case "rpm":
return "RPM"
case "amps":
return "A"
default:
return u
}
}
func sensorType(name, unit string) string {
u := strings.ToLower(unit)
n := strings.ToLower(name)
switch {
case strings.Contains(u, "degrees"):
return "temperature"
case strings.Contains(u, "volts"):
return "voltage"
case strings.Contains(u, "watts"):
return "power"
case strings.Contains(u, "rpm") || strings.Contains(n, "fan") && strings.Contains(n, "speed"):
return "fan"
case strings.Contains(u, "amps"):
return "current"
default:
return ""
}
}
// ── CPU ───────────────────────────────────────────────────────────────────────
// parseCPUInfo parses the comma-separated cpu_info file.
// Columns: slot, presence, model, processorID, cores, threads, flags, L1, L2, L3, partNum, devName, location, SN
func parseCPUInfo(content []byte) []models.CPU {
var cpus []models.CPU
lines := strings.Split(string(content), "\n")
for i, line := range lines {
if i == 0 { // skip header
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, ",")
if len(parts) < 6 {
continue
}
slot := strings.TrimSpace(parts[0])
if !strings.HasPrefix(strings.ToLower(slot), "cpu") {
continue
}
if strings.ToLower(strings.TrimSpace(parts[1])) != "present" {
continue
}
socketNum := 0
fmt.Sscanf(strings.ToLower(slot), "cpu%d", &socketNum)
model := strings.TrimSpace(parts[2])
cores := 0
fmt.Sscanf(strings.TrimSpace(parts[4]), "%d", &cores)
threads := 0
fmt.Sscanf(strings.TrimSpace(parts[5]), "%d", &threads)
l1, l2, l3 := 0, 0, 0
if len(parts) >= 10 {
l1 = parseCacheSizeKB(parts[7])
l2 = parseCacheSizeKB(parts[8])
l3 = parseCacheSizeKB(parts[9])
}
sn := ""
if len(parts) >= 14 {
sn = strings.TrimSpace(parts[13])
}
cpus = append(cpus, models.CPU{
Socket: socketNum,
Model: model,
Cores: cores,
Threads: threads,
L1CacheKB: l1,
L2CacheKB: l2,
L3CacheKB: l3,
SerialNumber: sn,
Status: "ok",
})
}
return cpus
}
func parseCacheSizeKB(s string) int {
var n int
fmt.Sscanf(strings.TrimSpace(s), "%d", &n)
return n
}
// ── Memory ────────────────────────────────────────────────────────────────────
// parseMemInfo parses the comma-separated mem_info file.
// Columns: slot, location, dimmName, manufacturer, size, maxSpeed, curSpeed, type, SN, voltage, rank, bitWidth, tech, bom, partNum, ..., health
func parseMemInfo(content []byte) []models.MemoryDIMM {
var dimms []models.MemoryDIMM
lines := strings.Split(string(content), "\n")
for i, line := range lines {
if i == 0 {
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, ",")
if len(parts) < 9 {
continue
}
sn := strings.TrimSpace(parts[8])
if strings.ToLower(sn) == "no dimm" || sn == "" {
continue
}
slot := strings.TrimSpace(parts[0])
location := strings.TrimSpace(parts[1])
manufacturer := strings.TrimSpace(parts[3])
if strings.ToLower(manufacturer) == "unknown" {
manufacturer = ""
}
sizeMB := 0
fmt.Sscanf(strings.TrimSpace(parts[4]), "%d MB", &sizeMB)
maxSpeedMHz := 0
fmt.Sscanf(strings.TrimSpace(parts[5]), "%d MT/s", &maxSpeedMHz)
curSpeedMHz := 0
fmt.Sscanf(strings.TrimSpace(parts[6]), "%d MT/s", &curSpeedMHz)
memType := strings.TrimSpace(parts[7])
if strings.ToLower(memType) == "unknown" {
memType = ""
}
ranks := 0
if len(parts) >= 11 {
fmt.Sscanf(strings.TrimSpace(parts[10]), "%d rank", &ranks)
}
partNum := ""
if len(parts) >= 15 {
v := strings.TrimSpace(parts[14])
if strings.ToLower(v) != "no dimm" && strings.ToLower(v) != "unknown" {
partNum = v
}
}
status := "ok"
if len(parts) >= 22 {
if s := strings.TrimSpace(parts[21]); strings.ToLower(s) != "ok" && s != "" {
status = strings.ToLower(s)
}
}
dimms = append(dimms, models.MemoryDIMM{
Slot: slot,
Location: location,
Present: true,
SizeMB: sizeMB,
Type: memType,
MaxSpeedMHz: maxSpeedMHz,
CurrentSpeedMHz: curSpeedMHz,
Manufacturer: manufacturer,
SerialNumber: sn,
PartNumber: partNum,
Ranks: ranks,
Status: status,
})
}
return dimms
}
// ── Card Info (GPU + NIC) ─────────────────────────────────────────────────────
// parseCardInfo parses card_info file, extracting GPU and OCP NIC card inventory.
// The file has named sections ("GPU Card Info", "OCP Card Info", etc.) each with a pipe-table.
func parseCardInfo(content []byte) (gpus []models.GPU, nicCards []xfusionNICCard) {
sections := splitPipeSections(content)
// Build BDF and VendorID/DeviceID map from PCIe Card Info: slot → info
type pcieEntry struct {
bdf string
vendorID int
deviceID int
desc string
}
slotPCIe := make(map[string]pcieEntry)
for _, row := range sections["pcie card info"] {
slot := strings.TrimSpace(row["slot"])
seg := parseHexInt(row["segment number"])
bus := parseHexInt(row["bus number"])
dev := parseHexInt(row["device number"])
fn := parseHexInt(row["function number"])
slotPCIe[slot] = pcieEntry{
bdf: fmt.Sprintf("%04x:%02x:%02x.%d", seg, bus, dev, fn),
vendorID: parseHexInt(row["vender id"]),
deviceID: parseHexInt(row["device id"]),
desc: strings.TrimSpace(row["card desc"]),
}
}
// GPU Card Info: slot, name, manufacturer, serialNum, firmVer, SBE/DBE counts
for _, row := range sections["gpu card info"] {
slot := strings.TrimSpace(row["slot"])
name := strings.TrimSpace(row["name"])
manufacturer := strings.TrimSpace(row["manufacturer"])
serial := strings.TrimSpace(row["serialnum"])
firmware := strings.TrimSpace(row["firmver"])
sbeCount, dbeCount := 0, 0
fmt.Sscanf(strings.TrimSpace(row["sbe"]), "%d", &sbeCount)
fmt.Sscanf(strings.TrimSpace(row["dbe"]), "%d", &dbeCount)
pcie := slotPCIe[slot]
gpu := models.GPU{
Slot: slot,
Model: name,
Manufacturer: manufacturer,
SerialNumber: serial,
Firmware: firmware,
BDF: pcie.bdf,
VendorID: pcie.vendorID,
DeviceID: pcie.deviceID,
Status: "ok",
}
if dbeCount > 0 {
gpu.Status = "warning"
}
gpus = append(gpus, gpu)
}
// OCP Card Info: NIC cards
for _, row := range sections["ocp card info"] {
slot := strings.TrimSpace(row["slot"])
pcie := slotPCIe[slot]
nicCards = append(nicCards, xfusionNICCard{
Slot: slot,
Model: strings.TrimSpace(row["card desc"]),
ProductName: strings.TrimSpace(row["card desc"]),
VendorID: parseHexInt(row["vender id"]),
DeviceID: parseHexInt(row["device id"]),
BDF: pcie.bdf,
SerialNumber: strings.TrimSpace(row["serialnumber"]),
PartNumber: strings.TrimSpace(row["partnum"]),
})
}
return gpus, nicCards
}
// splitPipeSections parses a multi-section file where each section starts with a
// plain header line (no "|") ending in "Info" or "info", followed by a pipe-table.
// Returns a map from lowercased section name → rows (each row is a map of lowercase header → value).
func splitPipeSections(content []byte) map[string][]map[string]string {
result := make(map[string][]map[string]string)
var sectionName string
var headers []string
for _, line := range strings.Split(string(content), "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if !strings.Contains(trimmed, "|") {
if strings.HasSuffix(strings.ToLower(trimmed), "info") {
sectionName = strings.ToLower(trimmed)
headers = nil
}
continue
}
parts := strings.Split(line, "|")
if len(parts) < 2 {
continue
}
cols := make([]string, len(parts))
for i, p := range parts {
cols[i] = strings.TrimSpace(p)
}
if headers == nil {
headers = make([]string, len(cols))
for i, h := range cols {
headers[i] = strings.ToLower(h)
}
continue
}
row := make(map[string]string, len(headers))
for i, h := range headers {
if i < len(cols) {
row[h] = cols[i]
}
}
result[sectionName] = append(result[sectionName], row)
}
return result
}
func parseHexInt(s string) int {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(strings.ToLower(s), "0x")
n, _ := strconv.ParseInt(s, 16, 64)
return int(n)
}
func parseNetcardInfo(content []byte) []xfusionNetcardSnapshot {
if len(content) == 0 {
return nil
}
var snapshots []xfusionNetcardSnapshot
var current *xfusionNetcardSnapshot
var currentPort *xfusionNetcardPort
flushPort := func() {
if current == nil || currentPort == nil {
return
}
current.Ports = append(current.Ports, *currentPort)
currentPort = nil
}
flushSnapshot := func() {
if current == nil || !current.hasData() {
return
}
flushPort()
snapshots = append(snapshots, *current)
current = nil
}
for _, rawLine := range strings.Split(string(content), "\n") {
line := strings.TrimSpace(rawLine)
if line == "" {
flushPort()
continue
}
if ts, ok := parseXFusionUTCTimestamp(line); ok {
if current == nil {
current = &xfusionNetcardSnapshot{Timestamp: ts}
continue
}
if current.hasData() {
flushSnapshot()
current = &xfusionNetcardSnapshot{Timestamp: ts}
continue
}
current.Timestamp = ts
continue
}
if current == nil {
current = &xfusionNetcardSnapshot{}
}
if port := parseNetcardPortHeader(line); port != nil {
flushPort()
currentPort = port
continue
}
if currentPort != nil {
if value, ok := parseSimpleKV(line, "MacAddr"); ok {
currentPort.MAC = value
continue
}
if value, ok := parseSimpleKV(line, "ActualMac"); ok {
currentPort.ActualMAC = value
continue
}
}
if value, ok := parseSimpleKV(line, "ProductName"); ok {
current.ProductName = value
continue
}
if value, ok := parseSimpleKV(line, "Manufacture"); ok {
current.Manufacturer = value
continue
}
if value, ok := parseSimpleKV(line, "FirmwareVersion"); ok {
current.Firmware = value
continue
}
if value, ok := parseSimpleKV(line, "SlotId"); ok {
current.Slot = value
}
}
flushSnapshot()
bestIndexBySlot := make(map[string]int)
for i, snapshot := range snapshots {
slot := strings.TrimSpace(snapshot.Slot)
if slot == "" {
continue
}
prevIdx, exists := bestIndexBySlot[slot]
if !exists || snapshot.isBetterThan(snapshots[prevIdx]) {
bestIndexBySlot[slot] = i
}
}
ordered := make([]xfusionNetcardSnapshot, 0, len(bestIndexBySlot))
for i, snapshot := range snapshots {
slot := strings.TrimSpace(snapshot.Slot)
bestIdx, ok := bestIndexBySlot[slot]
if !ok || bestIdx != i {
continue
}
ordered = append(ordered, snapshot)
delete(bestIndexBySlot, slot)
}
return ordered
}
func mergeNetworkAdapters(cards []xfusionNICCard, snapshots []xfusionNetcardSnapshot) ([]models.NetworkAdapter, []models.NIC) {
bySlotCard := make(map[string]xfusionNICCard, len(cards))
bySlotSnapshot := make(map[string]xfusionNetcardSnapshot, len(snapshots))
orderedSlots := make([]string, 0, len(cards)+len(snapshots))
seenSlots := make(map[string]struct{}, len(cards)+len(snapshots))
for _, card := range cards {
slot := strings.TrimSpace(card.Slot)
if slot == "" {
continue
}
bySlotCard[slot] = card
if _, seen := seenSlots[slot]; !seen {
orderedSlots = append(orderedSlots, slot)
seenSlots[slot] = struct{}{}
}
}
for _, snapshot := range snapshots {
slot := strings.TrimSpace(snapshot.Slot)
if slot == "" {
continue
}
bySlotSnapshot[slot] = snapshot
if _, seen := seenSlots[slot]; !seen {
orderedSlots = append(orderedSlots, slot)
seenSlots[slot] = struct{}{}
}
}
adapters := make([]models.NetworkAdapter, 0, len(orderedSlots))
legacyNICs := make([]models.NIC, 0, len(orderedSlots))
for _, slot := range orderedSlots {
card := bySlotCard[slot]
snapshot := bySlotSnapshot[slot]
model := firstNonEmpty(card.Model, snapshot.ProductName)
description := ""
if !strings.EqualFold(strings.TrimSpace(model), strings.TrimSpace(snapshot.ProductName)) {
description = strings.TrimSpace(snapshot.ProductName)
}
macs := snapshot.macAddresses()
bdf := firstNonEmpty(snapshot.primaryBDF(), card.BDF)
firmware := normalizeXFusionValue(snapshot.Firmware)
manufacturer := firstNonEmpty(snapshot.Manufacturer, card.Vendor)
portCount := len(snapshot.Ports)
if portCount == 0 && len(macs) > 0 {
portCount = len(macs)
}
if portCount == 0 {
portCount = 1
}
adapters = append(adapters, models.NetworkAdapter{
Slot: slot,
Location: "OCP",
Present: true,
BDF: bdf,
Model: model,
Description: description,
Vendor: manufacturer,
VendorID: card.VendorID,
DeviceID: card.DeviceID,
SerialNumber: card.SerialNumber,
PartNumber: card.PartNumber,
Firmware: firmware,
PortCount: portCount,
PortType: "ethernet",
MACAddresses: macs,
Status: "ok",
})
legacyNICs = append(legacyNICs, models.NIC{
Name: fmt.Sprintf("OCP%s", slot),
Model: model,
Description: description,
MACAddress: firstNonEmpty(macs...),
SerialNumber: card.SerialNumber,
})
}
return adapters, legacyNICs
}
func parseXFusionUTCTimestamp(line string) (time.Time, bool) {
ts, err := time.Parse("2006-01-02 15:04:05 MST", strings.TrimSpace(line))
if err != nil {
return time.Time{}, false
}
return ts, true
}
func parseNetcardPortHeader(line string) *xfusionNetcardPort {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) < 2 || !strings.HasPrefix(strings.ToLower(fields[0]), "port") {
return nil
}
joined := strings.Join(fields[1:], " ")
if !strings.HasPrefix(strings.ToLower(joined), "bdf:") {
return nil
}
return &xfusionNetcardPort{BDF: strings.TrimSpace(joined[len("BDF:"):])}
}
func parseSimpleKV(line, key string) (string, bool) {
idx := strings.Index(line, ":")
if idx < 0 {
return "", false
}
gotKey := strings.TrimSpace(line[:idx])
if !strings.EqualFold(gotKey, key) {
return "", false
}
return strings.TrimSpace(line[idx+1:]), true
}
func normalizeXFusionValue(value string) string {
value = strings.TrimSpace(value)
switch strings.ToUpper(value) {
case "", "N/A", "NA", "UNKNOWN":
return ""
default:
return value
}
}
func (s xfusionNetcardSnapshot) hasData() bool {
return strings.TrimSpace(s.Slot) != "" ||
strings.TrimSpace(s.ProductName) != "" ||
strings.TrimSpace(s.Manufacturer) != "" ||
strings.TrimSpace(s.Firmware) != "" ||
len(s.Ports) > 0
}
func (s xfusionNetcardSnapshot) score() int {
score := len(s.Ports)
if normalizeXFusionValue(s.Firmware) != "" {
score += 10
}
score += len(s.macAddresses()) * 2
return score
}
func (s xfusionNetcardSnapshot) isBetterThan(other xfusionNetcardSnapshot) bool {
if s.score() != other.score() {
return s.score() > other.score()
}
if !s.Timestamp.Equal(other.Timestamp) {
return s.Timestamp.After(other.Timestamp)
}
return len(s.Ports) > len(other.Ports)
}
func (s xfusionNetcardSnapshot) primaryBDF() string {
for _, port := range s.Ports {
if bdf := strings.TrimSpace(port.BDF); bdf != "" {
return bdf
}
}
return ""
}
func (s xfusionNetcardSnapshot) macAddresses() []string {
out := make([]string, 0, len(s.Ports))
seen := make(map[string]struct{}, len(s.Ports))
for _, port := range s.Ports {
for _, candidate := range []string{port.ActualMAC, port.MAC} {
mac := normalizeMAC(candidate)
if mac == "" {
continue
}
if _, exists := seen[mac]; exists {
continue
}
seen[mac] = struct{}{}
out = append(out, mac)
break
}
}
return out
}
func normalizeMAC(value string) string {
value = strings.ToUpper(strings.TrimSpace(value))
switch value {
case "", "N/A", "NA", "UNKNOWN", "00:00:00:00:00:00":
return ""
default:
return value
}
}
// ── PSU ───────────────────────────────────────────────────────────────────────
// parsePSUInfo parses the pipe-delimited psu_info.txt.
// Columns: Slot | presence | Manufacturer | Type | SN | Version | Rated Power | InputMode | PartNum | DeviceName | Vin | ...
func parsePSUInfo(content []byte) []models.PSU {
var psus []models.PSU
var headers []string
for _, line := range strings.Split(string(content), "\n") {
if !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
cols := make([]string, len(parts))
for i, p := range parts {
cols[i] = strings.TrimSpace(p)
}
if headers == nil {
headers = make([]string, len(cols))
for i, h := range cols {
headers[i] = strings.ToLower(h)
}
continue
}
row := make(map[string]string, len(headers))
for i, h := range headers {
if i < len(cols) {
row[h] = cols[i]
}
}
if strings.ToLower(row["presence"]) != "present" {
continue
}
wattage := 0
fmt.Sscanf(row["rated power"], "%d", &wattage)
inputVoltage := 0.0
fmt.Sscanf(row["vin"], "%f", &inputVoltage)
psus = append(psus, models.PSU{
Slot: row["slot"],
Present: true,
Model: row["type"],
Vendor: row["manufacturer"],
SerialNumber: row["sn"],
PartNumber: row["partnum"],
Firmware: row["version"],
WattageW: wattage,
InputType: row["inputmode"],
InputVoltage: inputVoltage,
Status: "ok",
})
}
return psus
}
// ── Storage ───────────────────────────────────────────────────────────────────
// parseStorageControllerInfo parses RAID_Controller_Info.txt and adds firmware entries.
func parseStorageControllerInfo(content []byte, result *models.AnalysisResult) {
// File may contain multiple controller blocks; parse key:value pairs from each.
// We only look at the first occurrence of each key (first controller).
seen := make(map[string]struct{}, len(result.Hardware.Firmware))
for _, fw := range result.Hardware.Firmware {
key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
seen[key] = struct{}{}
}
text := string(content)
blocks := strings.Split(text, "RAID Controller #")
for _, block := range blocks[1:] { // skip pre-block preamble
fields := parseKeyValueBlock([]byte(block))
name := firstNonEmpty(fields["Component Name"], fields["Controller Name"], fields["Controller Type"])
firmware := fields["Firmware Version"]
if name != "" && firmware != "" {
appendXFusionFirmware(result, seen, models.FirmwareInfo{
DeviceName: name,
Description: fields["Controller Name"],
Version: firmware,
})
}
}
}
func parseAppRevision(content []byte, result *models.AnalysisResult) {
type firmwareLine struct {
deviceName string
description string
buildKey string
}
known := map[string]firmwareLine{
"Active iBMC Version": {deviceName: "iBMC", description: "active iBMC", buildKey: "Active iBMC Built"},
"Active BIOS Version": {deviceName: "BIOS", description: "active BIOS", buildKey: "Active BIOS Built"},
"CPLD Version": {deviceName: "CPLD", description: "mainboard CPLD"},
"SDK Version": {deviceName: "SDK", description: "iBMC SDK", buildKey: "SDK Built"},
"Active Uboot Version": {deviceName: "U-Boot", description: "active U-Boot"},
"Active Secure Bootloader Version": {deviceName: "Secure Bootloader", description: "active secure bootloader"},
"Active Secure Firmware Version": {deviceName: "Secure Firmware", description: "active secure firmware"},
}
values := parseAlignedKeyValues(content)
if result.Hardware.BoardInfo.ProductName == "" {
if productName := values["Product Name"]; productName != "" {
result.Hardware.BoardInfo.ProductName = productName
}
}
seen := make(map[string]struct{}, len(result.Hardware.Firmware))
for _, fw := range result.Hardware.Firmware {
key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
seen[key] = struct{}{}
}
for key, meta := range known {
version := normalizeXFusionValue(values[key])
if version == "" {
continue
}
appendXFusionFirmware(result, seen, models.FirmwareInfo{
DeviceName: meta.deviceName,
Description: meta.description,
Version: version,
BuildTime: normalizeXFusionValue(values[meta.buildKey]),
})
}
}
func parseAlignedKeyValues(content []byte) map[string]string {
values := make(map[string]string)
for _, rawLine := range strings.Split(string(content), "\n") {
line := strings.TrimRight(rawLine, "\r")
if !strings.Contains(line, ":") {
continue
}
idx := strings.Index(line, ":")
if idx < 0 {
continue
}
key := strings.TrimRight(line[:idx], " \t")
value := strings.TrimSpace(line[idx+1:])
if key == "" || value == "" || values[key] != "" {
continue
}
values[key] = value
}
return values
}
func appendXFusionFirmware(result *models.AnalysisResult, seen map[string]struct{}, fw models.FirmwareInfo) {
if result == nil || result.Hardware == nil {
return
}
key := strings.ToLower(strings.TrimSpace(fw.DeviceName + "\x00" + fw.Version + "\x00" + fw.Description))
if key == "" {
return
}
if _, exists := seen[key]; exists {
return
}
seen[key] = struct{}{}
result.Hardware.Firmware = append(result.Hardware.Firmware, fw)
}
// parseDiskInfo parses a single PhysicalDrivesInfo/DiskN/disk_info file.
func parseDiskInfo(content []byte) *models.Storage {
fields := parseKeyValueBlock(content)
model := fields["Model"]
sn := fields["Serial Number"]
if model == "" && sn == "" {
return nil
}
sizeGB := 0
var capFloat float64
if _, err := fmt.Sscanf(fields["Capacity"], "%f GB", &capFloat); err == nil {
sizeGB = int(capFloat)
}
var wearPct *int
if wearStr := fields["Remnant Media Wearout"]; wearStr != "" {
var pct int
if _, err := fmt.Sscanf(wearStr, "%d%%", &pct); err == nil {
wearPct = &pct
}
}
status := "ok"
if h := strings.ToLower(fields["Health Status"]); h != "" && h != "normal" {
status = h
}
return &models.Storage{
Slot: firstNonEmpty(fields["Device Name"], fields["ID"]),
Type: fields["Media Type"],
Model: model,
SizeGB: sizeGB,
SerialNumber: sn,
Manufacturer: fields["Manufacturer"],
Firmware: fields["Firmware Version"],
Interface: fields["Interface Type"],
Present: true,
RemainingEndurancePct: wearPct,
Status: status,
}
}
// parseKeyValueBlock parses "Key (spaces) : Value" lines from a text block.
func parseKeyValueBlock(content []byte) map[string]string {
result := make(map[string]string)
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "=") || strings.HasPrefix(line, "-") {
continue
}
idx := strings.Index(line, " : ")
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+3:])
if key != "" && result[key] == "" {
result[key] = val
}
}
return result
}
// ── Events ────────────────────────────────────────────────────────────────────
// parseMaintenanceLog parses the iBMC maintenance_log file.
// Line format: "YYYY-MM-DD HH:MM:SS LEVEL : CODE,description"
func parseMaintenanceLog(content []byte) []models.Event {
var events []models.Event
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
if len(line) < 20 {
continue
}
ts, err := time.Parse("2006-01-02 15:04:05", line[:19])
if err != nil || ts.Year() <= 1970 {
continue // skip epoch-0 boot artifacts
}
rest := strings.TrimSpace(line[19:])
sepIdx := strings.Index(rest, " : ")
if sepIdx < 0 {
sepIdx = strings.Index(rest, ": ")
if sepIdx < 0 {
continue
}
} else {
sepIdx++ // skip leading space for " : "
}
levelStr := strings.TrimSpace(rest[:sepIdx-1])
body := strings.TrimSpace(rest[sepIdx+2:])
code := body
description := ""
if ci := strings.Index(body, ","); ci >= 0 {
code = body[:ci]
description = strings.TrimSpace(body[ci+1:])
}
var severity models.Severity
switch strings.ToUpper(levelStr) {
case "WARN", "WARNING":
severity = models.SeverityWarning
case "ERROR", "ERR", "CRIT", "CRITICAL":
severity = models.SeverityCritical
default:
severity = models.SeverityInfo
}
events = append(events, models.Event{
Timestamp: ts,
Source: "ibmc",
EventType: code,
Severity: severity,
Description: description,
RawData: line,
})
}
return events
}
// ── unused import guard ───────────────────────────────────────────────────────
var _ = parser.ExtractedFile{}