Add xFusion file-export parser support

This commit is contained in:
Mikhail Chusavitin
2026-04-04 15:07:10 +03:00
parent db74df9994
commit 7e9af89c46
9 changed files with 684 additions and 32 deletions

View File

@@ -10,6 +10,33 @@ import (
"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.
@@ -232,15 +259,15 @@ func parseCPUInfo(content []byte) []models.CPU {
}
cpus = append(cpus, models.CPU{
Socket: socketNum,
Model: model,
Cores: cores,
Threads: threads,
L1CacheKB: l1,
L2CacheKB: l2,
L3CacheKB: l3,
Socket: socketNum,
Model: model,
Cores: cores,
Threads: threads,
L1CacheKB: l1,
L2CacheKB: l2,
L3CacheKB: l3,
SerialNumber: sn,
Status: "ok",
Status: "ok",
})
}
return cpus
@@ -338,9 +365,9 @@ func parseMemInfo(content []byte) []models.MemoryDIMM {
// ── Card Info (GPU + NIC) ─────────────────────────────────────────────────────
// parseCardInfo parses card_info file, extracting GPU and NIC entries.
// 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, nics []models.NIC) {
func parseCardInfo(content []byte) (gpus []models.GPU, nicCards []xfusionNICCard) {
sections := splitPipeSections(content)
// Build BDF and VendorID/DeviceID map from PCIe Card Info: slot → info
@@ -396,17 +423,22 @@ func parseCardInfo(content []byte) (gpus []models.GPU, nics []models.NIC) {
}
// OCP Card Info: NIC cards
for i, row := range sections["ocp card info"] {
desc := strings.TrimSpace(row["card desc"])
sn := strings.TrimSpace(row["serialnumber"])
nics = append(nics, models.NIC{
Name: fmt.Sprintf("OCP%d", i+1),
Model: desc,
SerialNumber: sn,
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, nics
return gpus, nicCards
}
// splitPipeSections parses a multi-section file where each section starts with a
@@ -462,6 +494,301 @@ func parseHexInt(s string) int {
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.
@@ -525,6 +852,11 @@ func parsePSUInfo(content []byte) []models.PSU {
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
@@ -532,7 +864,7 @@ func parseStorageControllerInfo(content []byte, result *models.AnalysisResult) {
name := firstNonEmpty(fields["Component Name"], fields["Controller Name"], fields["Controller Type"])
firmware := fields["Firmware Version"]
if name != "" && firmware != "" {
result.Hardware.Firmware = append(result.Hardware.Firmware, models.FirmwareInfo{
appendXFusionFirmware(result, seen, models.FirmwareInfo{
DeviceName: name,
Description: fields["Controller Name"],
Version: firmware,
@@ -541,6 +873,86 @@ func parseStorageControllerInfo(content []byte, result *models.AnalysisResult) {
}
}
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)