Add xFusion file-export parser support
This commit is contained in:
450
internal/parser/vendors/xfusion/hardware.go
vendored
450
internal/parser/vendors/xfusion/hardware.go
vendored
@@ -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)
|
||||
|
||||
57
internal/parser/vendors/xfusion/parser.go
vendored
57
internal/parser/vendors/xfusion/parser.go
vendored
@@ -13,7 +13,7 @@ import (
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
const parserVersion = "1.0"
|
||||
const parserVersion = "1.1"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -34,11 +34,15 @@ func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||
path := strings.ToLower(f.Path)
|
||||
switch {
|
||||
case strings.Contains(path, "appdump/frudata/fruinfo.txt"):
|
||||
confidence += 60
|
||||
confidence += 50
|
||||
case strings.Contains(path, "rtosdump/versioninfo/app_revision.txt"):
|
||||
confidence += 30
|
||||
case strings.Contains(path, "appdump/sensor_alarm/sensor_info.txt"):
|
||||
confidence += 20
|
||||
confidence += 10
|
||||
case strings.Contains(path, "appdump/card_manage/card_info"):
|
||||
confidence += 20
|
||||
case strings.Contains(path, "logdump/netcard/netcard_info.txt"):
|
||||
confidence += 20
|
||||
}
|
||||
if confidence >= 100 {
|
||||
return 100
|
||||
@@ -54,17 +58,21 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
FRU: make([]models.FRUInfo, 0),
|
||||
Sensors: make([]models.SensorReading, 0),
|
||||
Hardware: &models.HardwareConfig{
|
||||
CPUs: make([]models.CPU, 0),
|
||||
Memory: make([]models.MemoryDIMM, 0),
|
||||
Storage: make([]models.Storage, 0),
|
||||
GPUs: make([]models.GPU, 0),
|
||||
NetworkCards: make([]models.NIC, 0),
|
||||
PowerSupply: make([]models.PSU, 0),
|
||||
Firmware: make([]models.FirmwareInfo, 0),
|
||||
Firmware: make([]models.FirmwareInfo, 0),
|
||||
Devices: make([]models.HardwareDevice, 0),
|
||||
CPUs: make([]models.CPU, 0),
|
||||
Memory: make([]models.MemoryDIMM, 0),
|
||||
Storage: make([]models.Storage, 0),
|
||||
Volumes: make([]models.StorageVolume, 0),
|
||||
PCIeDevices: make([]models.PCIeDevice, 0),
|
||||
GPUs: make([]models.GPU, 0),
|
||||
NetworkCards: make([]models.NIC, 0),
|
||||
NetworkAdapters: make([]models.NetworkAdapter, 0),
|
||||
PowerSupply: make([]models.PSU, 0),
|
||||
},
|
||||
}
|
||||
|
||||
if f := findByPath(files, "appdump/frudata/fruinfo.txt"); f != nil {
|
||||
if f := findByAnyPath(files, "appdump/frudata/fruinfo.txt", "rtosdump/versioninfo/fruinfo.txt"); f != nil {
|
||||
parseFRUInfo(f.Content, result)
|
||||
}
|
||||
if f := findByPath(files, "appdump/sensor_alarm/sensor_info.txt"); f != nil {
|
||||
@@ -76,10 +84,20 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
if f := findByPath(files, "appdump/cpumem/mem_info"); f != nil {
|
||||
result.Hardware.Memory = parseMemInfo(f.Content)
|
||||
}
|
||||
var nicCards []xfusionNICCard
|
||||
if f := findByPath(files, "appdump/card_manage/card_info"); f != nil {
|
||||
gpus, nics := parseCardInfo(f.Content)
|
||||
gpus, cards := parseCardInfo(f.Content)
|
||||
result.Hardware.GPUs = gpus
|
||||
result.Hardware.NetworkCards = nics
|
||||
nicCards = cards
|
||||
}
|
||||
if f := findByPath(files, "logdump/netcard/netcard_info.txt"); f != nil || len(nicCards) > 0 {
|
||||
var content []byte
|
||||
if f != nil {
|
||||
content = f.Content
|
||||
}
|
||||
adapters, legacyNICs := mergeNetworkAdapters(nicCards, parseNetcardInfo(content))
|
||||
result.Hardware.NetworkAdapters = adapters
|
||||
result.Hardware.NetworkCards = legacyNICs
|
||||
}
|
||||
if f := findByPath(files, "appdump/bmc/psu_info.txt"); f != nil {
|
||||
result.Hardware.PowerSupply = parsePSUInfo(f.Content)
|
||||
@@ -87,6 +105,9 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
if f := findByPath(files, "appdump/storagemgnt/raid_controller_info.txt"); f != nil {
|
||||
parseStorageControllerInfo(f.Content, result)
|
||||
}
|
||||
if f := findByPath(files, "rtosdump/versioninfo/app_revision.txt"); f != nil {
|
||||
parseAppRevision(f.Content, result)
|
||||
}
|
||||
for _, f := range findDiskInfoFiles(files) {
|
||||
disk := parseDiskInfo(f.Content)
|
||||
if disk != nil {
|
||||
@@ -99,6 +120,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
|
||||
result.Protocol = "ipmi"
|
||||
result.SourceType = models.SourceTypeArchive
|
||||
parser.ApplyManufacturedYearWeekFromFRU(result.FRU, result.Hardware)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -113,6 +135,15 @@ func findByPath(files []parser.ExtractedFile, substring string) *parser.Extracte
|
||||
return nil
|
||||
}
|
||||
|
||||
func findByAnyPath(files []parser.ExtractedFile, substrings ...string) *parser.ExtractedFile {
|
||||
for _, substring := range substrings {
|
||||
if f := findByPath(files, substring); f != nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findDiskInfoFiles returns all PhysicalDrivesInfo disk_info files.
|
||||
func findDiskInfoFiles(files []parser.ExtractedFile) []parser.ExtractedFile {
|
||||
var out []parser.ExtractedFile
|
||||
|
||||
113
internal/parser/vendors/xfusion/parser_test.go
vendored
113
internal/parser/vendors/xfusion/parser_test.go
vendored
@@ -1,8 +1,10 @@
|
||||
package xfusion
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,29 @@ func TestDetect_G5500V7(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetect_ServerFileExportMarkers(t *testing.T) {
|
||||
p := &Parser{}
|
||||
score := p.Detect([]parser.ExtractedFile{
|
||||
{Path: "dump_info/RTOSDump/versioninfo/app_revision.txt", Content: []byte("Product Name: G5500 V7")},
|
||||
{Path: "dump_info/LogDump/netcard/netcard_info.txt", Content: []byte("2026-02-04 03:54:06 UTC")},
|
||||
{Path: "dump_info/AppDump/card_manage/card_info", Content: []byte("OCP Card Info")},
|
||||
})
|
||||
if score < 70 {
|
||||
t.Fatalf("expected Detect score >= 70 for xFusion file export markers, got %d", score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetect_Negative(t *testing.T) {
|
||||
p := &Parser{}
|
||||
score := p.Detect([]parser.ExtractedFile{
|
||||
{Path: "logs/messages.txt", Content: []byte("plain text")},
|
||||
{Path: "inventory.json", Content: []byte(`{"vendor":"other"}`)},
|
||||
})
|
||||
if score != 0 {
|
||||
t.Fatalf("expected Detect score 0 for non-xFusion input, got %d", score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_G5500V7_BoardInfo(t *testing.T) {
|
||||
files := loadTestArchive(t, "../../../../example/G5500V7_210619KUGGXGS2000015_20260318-1128.tar.gz")
|
||||
p := &Parser{}
|
||||
@@ -126,6 +151,94 @@ func TestParse_G5500V7_NICs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_ServerFileExport_NetworkAdaptersAndFirmware(t *testing.T) {
|
||||
p := &Parser{}
|
||||
files := []parser.ExtractedFile{
|
||||
{
|
||||
Path: "dump_info/AppDump/card_manage/card_info",
|
||||
Content: []byte(strings.TrimSpace(`
|
||||
Pcie Card Info
|
||||
Slot | Vender Id | Device Id | Sub Vender Id | Sub Device Id | Segment Number | Bus Number | Device Number | Function Number | Card Desc | Board Id | PCB Version | CPLD Version | Sub Card Bom Id | PartNum | SerialNumber | OriginalPartNum
|
||||
1 | 0x15b3 | 0x101f | 0x1f24 | 0x2011 | 0x00 | 0x27 | 0x00 | 0x00 | MT2894 Family [ConnectX-6 Lx] | N/A | N/A | N/A | N/A | 0302Y238 | 02Y238X6RC000058 |
|
||||
|
||||
OCP Card Info
|
||||
Slot | Vender Id | Device Id | Sub Vender Id | Sub Device Id | Segment Number | Bus Number | Device Number | Function Number | Card Desc | Board Id | PCB Version | CPLD Version | Sub Card Bom Id | PartNum | SerialNumber | OriginalPartNum
|
||||
1 | 0x15b3 | 0x101f | 0x1f24 | 0x2011 | 0x00 | 0x27 | 0x00 | 0x00 | MT2894 Family [ConnectX-6 Lx] | N/A | N/A | N/A | N/A | 0302Y238 | 02Y238X6RC000058 |
|
||||
`)),
|
||||
},
|
||||
{
|
||||
Path: "dump_info/LogDump/netcard/netcard_info.txt",
|
||||
Content: []byte(strings.TrimSpace(`
|
||||
2026-02-04 03:54:06 UTC
|
||||
ProductName :XC385
|
||||
Manufacture :XFUSION
|
||||
FirmwareVersion :26.39.2048
|
||||
SlotId :1
|
||||
Port0 BDF:0000:27:00.0
|
||||
MacAddr:44:1A:4C:16:E8:03
|
||||
ActualMac:44:1A:4C:16:E8:03
|
||||
Port1 BDF:0000:27:00.1
|
||||
MacAddr:00:00:00:00:00:00
|
||||
ActualMac:44:1A:4C:16:E8:04
|
||||
`)),
|
||||
},
|
||||
{
|
||||
Path: "dump_info/RTOSDump/versioninfo/app_revision.txt",
|
||||
Content: []byte(strings.TrimSpace(`
|
||||
------------------- iBMC INFO -------------------
|
||||
Active iBMC Version: (U68)3.08.05.85
|
||||
Active iBMC Built: 16:46:26 Jan 4 2026
|
||||
SDK Version: 13.16.30.16
|
||||
SDK Built: 07:55:18 Dec 12 2025
|
||||
Active BIOS Version: (U6216)01.02.08.17
|
||||
Active BIOS Built: 00:00:00 Jan 05 2026
|
||||
Product Name: G5500 V7
|
||||
`)),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := p.Parse(files)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if result.Protocol != "ipmi" || result.SourceType != models.SourceTypeArchive {
|
||||
t.Fatalf("unexpected source metadata: protocol=%q source_type=%q", result.Protocol, result.SourceType)
|
||||
}
|
||||
if result.Hardware == nil {
|
||||
t.Fatal("Hardware is nil")
|
||||
}
|
||||
if len(result.Hardware.NetworkAdapters) != 1 {
|
||||
t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
|
||||
}
|
||||
adapter := result.Hardware.NetworkAdapters[0]
|
||||
if adapter.BDF != "0000:27:00.0" {
|
||||
t.Fatalf("expected network adapter BDF 0000:27:00.0, got %q", adapter.BDF)
|
||||
}
|
||||
if adapter.Firmware != "26.39.2048" {
|
||||
t.Fatalf("expected network adapter firmware 26.39.2048, got %q", adapter.Firmware)
|
||||
}
|
||||
if adapter.SerialNumber != "02Y238X6RC000058" {
|
||||
t.Fatalf("expected network adapter serial from card_info, got %q", adapter.SerialNumber)
|
||||
}
|
||||
if len(adapter.MACAddresses) != 2 || adapter.MACAddresses[0] != "44:1A:4C:16:E8:03" || adapter.MACAddresses[1] != "44:1A:4C:16:E8:04" {
|
||||
t.Fatalf("unexpected MAC addresses: %#v", adapter.MACAddresses)
|
||||
}
|
||||
|
||||
fwByDevice := make(map[string]models.FirmwareInfo)
|
||||
for _, fw := range result.Hardware.Firmware {
|
||||
fwByDevice[fw.DeviceName] = fw
|
||||
}
|
||||
if fwByDevice["iBMC"].Version != "(U68)3.08.05.85" {
|
||||
t.Fatalf("expected iBMC firmware from app_revision.txt, got %#v", fwByDevice["iBMC"])
|
||||
}
|
||||
if fwByDevice["BIOS"].Version != "(U6216)01.02.08.17" {
|
||||
t.Fatalf("expected BIOS firmware from app_revision.txt, got %#v", fwByDevice["BIOS"])
|
||||
}
|
||||
if result.Hardware.BoardInfo.ProductName != "G5500 V7" {
|
||||
t.Fatalf("expected board product fallback from app_revision.txt, got %q", result.Hardware.BoardInfo.ProductName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_G5500V7_PSUs(t *testing.T) {
|
||||
files := loadTestArchive(t, "../../../../example/G5500V7_210619KUGGXGS2000015_20260318-1128.tar.gz")
|
||||
p := &Parser{}
|
||||
|
||||
@@ -44,6 +44,9 @@ func TestParserParseExample(t *testing.T) {
|
||||
examplePath := filepath.Join("..", "..", "..", "..", "example", "xigmanas.txt")
|
||||
raw, err := os.ReadFile(examplePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Skipf("example file %s not present", examplePath)
|
||||
}
|
||||
t.Fatalf("read example file: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
Source: "network_adapters",
|
||||
Slot: nic.Slot,
|
||||
Location: nic.Location,
|
||||
BDF: nic.BDF,
|
||||
DeviceClass: "NetworkController",
|
||||
VendorID: nic.VendorID,
|
||||
DeviceID: nic.DeviceID,
|
||||
@@ -254,6 +255,11 @@ func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
PortCount: nic.PortCount,
|
||||
PortType: nic.PortType,
|
||||
MACAddresses: nic.MACAddresses,
|
||||
LinkWidth: nic.LinkWidth,
|
||||
LinkSpeed: nic.LinkSpeed,
|
||||
MaxLinkWidth: nic.MaxLinkWidth,
|
||||
MaxLinkSpeed: nic.MaxLinkSpeed,
|
||||
NUMANode: nic.NUMANode,
|
||||
Present: &present,
|
||||
Status: nic.Status,
|
||||
StatusCheckedAt: nic.StatusCheckedAt,
|
||||
|
||||
@@ -122,6 +122,41 @@ func TestBuildHardwareDevices_ZeroSizeMemoryWithInventoryIsIncluded(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHardwareDevices_NetworkAdapterPreservesPCIeMetadata(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
NetworkAdapters: []models.NetworkAdapter{
|
||||
{
|
||||
Slot: "1",
|
||||
Location: "OCP",
|
||||
Present: true,
|
||||
BDF: "0000:27:00.0",
|
||||
Model: "ConnectX-6 Lx",
|
||||
VendorID: 0x15b3,
|
||||
DeviceID: 0x101f,
|
||||
SerialNumber: "NIC-001",
|
||||
Firmware: "26.39.2048",
|
||||
MACAddresses: []string{"44:1A:4C:16:E8:03", "44:1A:4C:16:E8:04"},
|
||||
LinkWidth: 16,
|
||||
LinkSpeed: "32 GT/s",
|
||||
NUMANode: 1,
|
||||
Status: "ok",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
devices := BuildHardwareDevices(hw)
|
||||
for _, d := range devices {
|
||||
if d.Kind != models.DeviceKindNetwork {
|
||||
continue
|
||||
}
|
||||
if d.BDF != "0000:27:00.0" || d.LinkWidth != 16 || d.LinkSpeed != "32 GT/s" || d.NUMANode != 1 {
|
||||
t.Fatalf("expected network PCIe metadata to be preserved, got %+v", d)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal("expected network device in canonical inventory")
|
||||
}
|
||||
|
||||
func TestBuildSpecification_ZeroSizeMemoryWithInventoryIsShown(t *testing.T) {
|
||||
hw := &models.HardwareConfig{
|
||||
Memory: []models.MemoryDIMM{
|
||||
|
||||
Reference in New Issue
Block a user