package inspur import ( "encoding/json" "regexp" "strings" "git.mchus.pro/mchus/logpile/internal/models" ) // componentLogFRURegex isolates the JSON array that follows "RESTful FRU info:" // in a combined component.log. FRU areas carry no nested arrays, so the lazy // match stops at the array's own closing bracket. var componentLogFRURegex = regexp.MustCompile(`RESTful FRU info:\s*(\[[\s\S]*?\])`) type componentFRUArea struct { Version int `json:"version"` Type string `json:"type"` Date string `json:"date"` Manufacturer string `json:"manufacturer"` ProductName string `json:"product_name"` SerialNumber string `json:"serial_number"` PartNumber string `json:"part_number"` AssetTag string `json:"asset_tag"` } type componentFRUDevice struct { Device struct { ID int `json:"id"` Name string `json:"name"` SystemUUID string `json:"system_uuid"` } `json:"device"` Chassis componentFRUArea `json:"chassis"` Board componentFRUArea `json:"board"` Product componentFRUArea `json:"product"` } // ParseComponentLogFRU parses the "RESTful FRU info:" JSON block from a combined // component.log. Some onekeylog dumps ship this block instead of a // devicefrusdr.log / asset.json, so it is the only source of board manufacturer, // product name, part number and system UUID for that dump class. It also sets // hw.BoardInfo.UUID directly, which extractBoardInfo does not populate. func ParseComponentLogFRU(content []byte, hw *models.HardwareConfig) []models.FRUInfo { m := componentLogFRURegex.FindSubmatch(content) if m == nil { return nil } var devices []componentFRUDevice if err := json.Unmarshal(m[1], &devices); err != nil { return nil } var out []models.FRUInfo for _, d := range devices { fru := componentFRUToModel(d) if fru.Manufacturer == "" && fru.ProductName == "" && fru.SerialNumber == "" && fru.PartNumber == "" { continue } out = append(out, fru) if hw != nil && hw.BoardInfo.UUID == "" && isSystemFRUDevice(d.Device.Name) { if u := fruAreaValue(d.Device.SystemUUID); u != "" { hw.BoardInfo.UUID = u } } } return out } func isSystemFRUDevice(name string) bool { n := strings.ToUpper(strings.TrimSpace(name)) return n == "BMC_FRU" || n == "MB_FRU" || strings.HasPrefix(n, "MAINBOARD") } // componentFRUToModel flattens a FRU device into one models.FRUInfo. The product // area (system serial / asset tag the operator knows, e.g. "24C319579") is // preferred over the board area (physical PCB serial) for identity, matching the // BEE-SP live-CD inventory and the Reanimator board contract. func componentFRUToModel(d componentFRUDevice) models.FRUInfo { desc := strings.TrimSpace(d.Device.Name) if desc == "" { desc = "FRU Device" } // extractBoardInfo only lifts manufacturer from an entry it recognises as the // main board; tag the system FRU so it does. if isSystemFRUDevice(d.Device.Name) { desc += " (builtin board)" } return models.FRUInfo{ Description: desc, ChassisType: fruAreaValue(d.Chassis.Type), Manufacturer: fruAreaValue(d.Product.Manufacturer, d.Board.Manufacturer), ProductName: fruAreaValue(d.Product.ProductName, d.Board.ProductName), SerialNumber: fruAreaValue(d.Product.SerialNumber, d.Board.SerialNumber), PartNumber: fruAreaValue(d.Product.PartNumber, d.Board.PartNumber, d.Chassis.PartNumber), MfgDate: fruAreaValue(d.Board.Date), AssetTag: fruAreaValue(d.Product.AssetTag), } } // fruAreaValue returns the first argument that is a real value. iBMC uses "0", // "NULL" and whitespace-padded blanks as placeholders in unused FRU areas. func fruAreaValue(values ...string) string { for _, v := range values { t := strings.TrimSpace(v) if t == "" || t == "0" || strings.EqualFold(t, "null") || strings.EqualFold(t, "n/a") { continue } return t } return "" }