Improve Redfish recovery flow and raw export timing diagnostics
This commit is contained in:
50
internal/parser/vendors/inspur/asset.go
vendored
50
internal/parser/vendors/inspur/asset.go
vendored
@@ -58,6 +58,7 @@ type AssetJSON struct {
|
||||
} `json:"MemInfo"`
|
||||
|
||||
HddInfo []struct {
|
||||
PresentBitmap []int `json:"PresentBitmap"`
|
||||
SerialNumber string `json:"SerialNumber"`
|
||||
Manufacturer string `json:"Manufacturer"`
|
||||
ModelName string `json:"ModelName"`
|
||||
@@ -163,6 +164,18 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
||||
// Parse storage info
|
||||
seenHDDFW := make(map[string]bool)
|
||||
for _, hdd := range asset.HddInfo {
|
||||
slot := normalizeAssetHDDSlot(hdd.LocationString, hdd.Location, hdd.DiskInterfaceType)
|
||||
modelName := strings.TrimSpace(hdd.ModelName)
|
||||
serial := normalizeRedisValue(hdd.SerialNumber)
|
||||
present := bitmapHasAnyValue(hdd.PresentBitmap)
|
||||
if !present && (slot != "" || modelName != "" || serial != "" || hdd.Capacity > 0) {
|
||||
present = true
|
||||
}
|
||||
|
||||
if !present && slot == "" && modelName == "" && serial == "" && hdd.Capacity == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
storageType := "HDD"
|
||||
if hdd.DiskInterfaceType == 5 {
|
||||
storageType = "NVMe"
|
||||
@@ -171,30 +184,30 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
||||
}
|
||||
|
||||
// Resolve manufacturer: try vendor ID first, then model name extraction
|
||||
modelName := strings.TrimSpace(hdd.ModelName)
|
||||
manufacturer := resolveManufacturer(hdd.Manufacturer, modelName)
|
||||
|
||||
config.Storage = append(config.Storage, models.Storage{
|
||||
Slot: hdd.LocationString,
|
||||
Slot: slot,
|
||||
Type: storageType,
|
||||
Model: modelName,
|
||||
SizeGB: hdd.Capacity,
|
||||
SerialNumber: hdd.SerialNumber,
|
||||
SerialNumber: serial,
|
||||
Manufacturer: manufacturer,
|
||||
Firmware: hdd.FirmwareVersion,
|
||||
Interface: diskInterfaceToString(hdd.DiskInterfaceType),
|
||||
Present: present,
|
||||
})
|
||||
|
||||
// Add HDD firmware to firmware list (deduplicated by model+version)
|
||||
if hdd.FirmwareVersion != "" {
|
||||
fwKey := modelName + ":" + hdd.FirmwareVersion
|
||||
if !seenHDDFW[fwKey] {
|
||||
slot := hdd.LocationString
|
||||
if slot == "" {
|
||||
slot = fmt.Sprintf("%s %dGB", storageType, hdd.Capacity)
|
||||
fwSlot := slot
|
||||
if fwSlot == "" {
|
||||
fwSlot = fmt.Sprintf("%s %dGB", storageType, hdd.Capacity)
|
||||
}
|
||||
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
||||
DeviceName: fmt.Sprintf("%s (%s)", modelName, slot),
|
||||
DeviceName: fmt.Sprintf("%s (%s)", modelName, fwSlot),
|
||||
Version: hdd.FirmwareVersion,
|
||||
})
|
||||
seenHDDFW[fwKey] = true
|
||||
@@ -323,6 +336,29 @@ func diskInterfaceToString(ifType int) string {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAssetHDDSlot(locationString string, location int, diskInterfaceType int) string {
|
||||
slot := strings.TrimSpace(locationString)
|
||||
if slot != "" {
|
||||
return slot
|
||||
}
|
||||
if location < 0 {
|
||||
return ""
|
||||
}
|
||||
if diskInterfaceType == 5 {
|
||||
return fmt.Sprintf("OB%02d", location+1)
|
||||
}
|
||||
return fmt.Sprintf("%d", location)
|
||||
}
|
||||
|
||||
func bitmapHasAnyValue(values []int) bool {
|
||||
for _, v := range values {
|
||||
if v != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pcieLinkSpeedToString(speed int) string {
|
||||
switch speed {
|
||||
case 1:
|
||||
|
||||
98
internal/parser/vendors/inspur/component.go
vendored
98
internal/parser/vendors/inspur/component.go
vendored
@@ -221,15 +221,19 @@ func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||
})
|
||||
for _, hdd := range hddInfo {
|
||||
if hdd.Present == 1 {
|
||||
hddMap[hdd.LocationString] = struct {
|
||||
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: strings.TrimSpace(hdd.SN),
|
||||
SN: normalizeRedisValue(hdd.SN),
|
||||
Model: strings.TrimSpace(hdd.Model),
|
||||
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||
Firmware: normalizeRedisValue(hdd.Firmware),
|
||||
Mfr: strings.TrimSpace(hdd.Manufacture),
|
||||
}
|
||||
}
|
||||
@@ -245,18 +249,19 @@ func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if hw.Storage[i].SerialNumber == "" {
|
||||
if normalizeRedisValue(hw.Storage[i].SerialNumber) == "" {
|
||||
hw.Storage[i].SerialNumber = detail.SN
|
||||
}
|
||||
if hw.Storage[i].Model == "" {
|
||||
hw.Storage[i].Model = detail.Model
|
||||
}
|
||||
if hw.Storage[i].Firmware == "" {
|
||||
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
|
||||
@@ -275,16 +280,21 @@ func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||
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: hdd.LocationString,
|
||||
Slot: slot,
|
||||
Type: storType,
|
||||
Model: model,
|
||||
SizeGB: hdd.Capacity,
|
||||
SerialNumber: strings.TrimSpace(hdd.SN),
|
||||
SerialNumber: normalizeRedisValue(hdd.SN),
|
||||
Manufacturer: extractStorageManufacturer(model),
|
||||
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||
Firmware: normalizeRedisValue(hdd.Firmware),
|
||||
Interface: iface,
|
||||
Present: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -732,28 +742,88 @@ func parseDiskBackplaneInfo(text string, hw *models.HardwareConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create storage entries based on backplane info
|
||||
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"
|
||||
}
|
||||
|
||||
// Create entries for each port (disk slot)
|
||||
for i := 0; i < bp.PortCount; i++ {
|
||||
isPresent := i < bp.DriverCount
|
||||
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: fmt.Sprintf("%d", i),
|
||||
Present: isPresent,
|
||||
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
|
||||
}
|
||||
|
||||
3
internal/parser/vendors/inspur/parser.go
vendored
3
internal/parser/vendors/inspur/parser.go
vendored
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||
const parserVersion = "1.3.0"
|
||||
const parserVersion = "1.4.0"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -178,6 +178,7 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
// Mark problematic GPUs from IDL errors like "BIOS miss F_GPU6".
|
||||
if result.Hardware != nil {
|
||||
applyGPUStatusFromEvents(result.Hardware, result.Events)
|
||||
enrichStorageFromSerialFallbackFiles(files, result.Hardware)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
148
internal/parser/vendors/inspur/storage_serial_fallback.go
vendored
Normal file
148
internal/parser/vendors/inspur/storage_serial_fallback.go
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
var bpHDDSerialTokenRegex = regexp.MustCompile(`[A-Za-z0-9]{8,32}`)
|
||||
|
||||
func enrichStorageFromSerialFallbackFiles(files []parser.ExtractedFile, hw *models.HardwareConfig) {
|
||||
if hw == nil {
|
||||
return
|
||||
}
|
||||
f := parser.FindFileByName(files, "BpHDDSerialNumber.info")
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
serials := extractBPHDDSerials(f.Content)
|
||||
if len(serials) == 0 {
|
||||
return
|
||||
}
|
||||
applyStorageSerialFallback(hw, serials)
|
||||
}
|
||||
|
||||
func extractBPHDDSerials(content []byte) []string {
|
||||
if len(content) == 0 {
|
||||
return nil
|
||||
}
|
||||
matches := bpHDDSerialTokenRegex.FindAllString(string(content), -1)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(matches))
|
||||
seen := make(map[string]struct{}, len(matches))
|
||||
for _, m := range matches {
|
||||
v := normalizeRedisValue(m)
|
||||
if !looksLikeStorageSerial(v) {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(v)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func looksLikeStorageSerial(v string) bool {
|
||||
if len(v) < 8 {
|
||||
return false
|
||||
}
|
||||
hasLetter := false
|
||||
hasDigit := false
|
||||
for _, r := range v {
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
hasLetter = true
|
||||
case r >= 'a' && r <= 'z':
|
||||
hasLetter = true
|
||||
case r >= '0' && r <= '9':
|
||||
hasDigit = true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return hasLetter && hasDigit
|
||||
}
|
||||
|
||||
func applyStorageSerialFallback(hw *models.HardwareConfig, serials []string) {
|
||||
if hw == nil || len(hw.Storage) == 0 || len(serials) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
existing := make(map[string]struct{}, len(hw.Storage))
|
||||
for _, dev := range hw.Storage {
|
||||
if sn := normalizeRedisValue(dev.SerialNumber); sn != "" {
|
||||
existing[strings.ToLower(sn)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(serials))
|
||||
for _, sn := range serials {
|
||||
key := strings.ToLower(sn)
|
||||
if _, ok := existing[key]; ok {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, sn)
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type target struct {
|
||||
index int
|
||||
rank int
|
||||
slot string
|
||||
}
|
||||
targets := make([]target, 0, len(hw.Storage))
|
||||
for i := range hw.Storage {
|
||||
dev := hw.Storage[i]
|
||||
if normalizeRedisValue(dev.SerialNumber) != "" {
|
||||
continue
|
||||
}
|
||||
if !dev.Present && strings.TrimSpace(dev.Slot) == "" {
|
||||
continue
|
||||
}
|
||||
rank := 0
|
||||
if !dev.Present {
|
||||
rank += 10
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(dev.Type), "NVMe") {
|
||||
rank += 5
|
||||
}
|
||||
if strings.TrimSpace(dev.Slot) == "" {
|
||||
rank += 4
|
||||
}
|
||||
targets = append(targets, target{
|
||||
index: i,
|
||||
rank: rank,
|
||||
slot: strings.ToLower(strings.TrimSpace(dev.Slot)),
|
||||
})
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(targets, func(i, j int) bool {
|
||||
if targets[i].rank != targets[j].rank {
|
||||
return targets[i].rank < targets[j].rank
|
||||
}
|
||||
return targets[i].slot < targets[j].slot
|
||||
})
|
||||
|
||||
for i := 0; i < len(targets) && i < len(filtered); i++ {
|
||||
dev := &hw.Storage[targets[i].index]
|
||||
dev.SerialNumber = filtered[i]
|
||||
if !dev.Present {
|
||||
dev.Present = true
|
||||
}
|
||||
}
|
||||
}
|
||||
106
internal/parser/vendors/inspur/storage_serial_fallback_test.go
vendored
Normal file
106
internal/parser/vendors/inspur/storage_serial_fallback_test.go
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
)
|
||||
|
||||
func TestParseAssetJSON_HddSlotFallbackAndPresence(t *testing.T) {
|
||||
content := []byte(`{
|
||||
"HddInfo": [
|
||||
{
|
||||
"PresentBitmap": [1],
|
||||
"SerialNumber": "",
|
||||
"Manufacturer": "",
|
||||
"ModelName": "",
|
||||
"FirmwareVersion": "",
|
||||
"Capacity": 0,
|
||||
"Location": 2,
|
||||
"DiskInterfaceType": 5,
|
||||
"MediaType": 1,
|
||||
"LocationString": ""
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
hw, err := ParseAssetJSON(content)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseAssetJSON failed: %v", err)
|
||||
}
|
||||
if len(hw.Storage) != 1 {
|
||||
t.Fatalf("expected 1 storage entry, got %d", len(hw.Storage))
|
||||
}
|
||||
if hw.Storage[0].Slot != "OB03" {
|
||||
t.Fatalf("expected OB03 slot fallback, got %q", hw.Storage[0].Slot)
|
||||
}
|
||||
if !hw.Storage[0].Present {
|
||||
t.Fatalf("expected fallback storage entry marked present")
|
||||
}
|
||||
if hw.Storage[0].Type != "NVMe" {
|
||||
t.Fatalf("expected NVMe type, got %q", hw.Storage[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDiskBackplaneInfo_PopulatesOnlyMissingPresentDrives(t *testing.T) {
|
||||
text := `RESTful diskbackplane info:
|
||||
[
|
||||
{ "port_count": 8, "driver_count": 4, "front": 1, "backplane_index": 0, "present": 1, "cpld_version": "3.1", "temperature": 18 },
|
||||
{ "port_count": 8, "driver_count": 3, "front": 1, "backplane_index": 1, "present": 1, "cpld_version": "3.1", "temperature": 17 }
|
||||
]
|
||||
BMC`
|
||||
|
||||
hw := &models.HardwareConfig{
|
||||
Storage: []models.Storage{
|
||||
{Slot: "OB01", Type: "NVMe", Present: true},
|
||||
{Slot: "OB02", Type: "NVMe", Present: true},
|
||||
{Slot: "OB03", Type: "NVMe", Present: true},
|
||||
{Slot: "OB04", Type: "NVMe", Present: true},
|
||||
},
|
||||
}
|
||||
|
||||
parseDiskBackplaneInfo(text, hw)
|
||||
|
||||
if len(hw.Storage) != 7 {
|
||||
t.Fatalf("expected total storage count 7 after backplane merge, got %d", len(hw.Storage))
|
||||
}
|
||||
bpCount := 0
|
||||
for _, dev := range hw.Storage {
|
||||
if strings.HasPrefix(dev.Slot, "BP0:") || strings.HasPrefix(dev.Slot, "BP1:") {
|
||||
bpCount++
|
||||
}
|
||||
}
|
||||
if bpCount != 3 {
|
||||
t.Fatalf("expected 3 synthetic backplane rows, got %d", bpCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichStorageFromSerialFallbackFiles_AssignsSerials(t *testing.T) {
|
||||
files := []parser.ExtractedFile{
|
||||
{
|
||||
Path: "onekeylog/configuration/conf/BpHDDSerialNumber.info",
|
||||
Content: []byte{
|
||||
0xA0, 0xA1, 0xA2, 0xA3,
|
||||
'S', '6', 'K', 'N', 'N', 'G', '0', 'W', '4', '2', '8', '5', '5', '2',
|
||||
0x00,
|
||||
'P', 'H', 'Y', 'I', '5', '2', '7', '1', '0', '0', '4', 'B', '1', 'P', '9', 'D', 'G', 'N',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hw := &models.HardwareConfig{
|
||||
Storage: []models.Storage{
|
||||
{Slot: "BP0:0", Type: "HDD", Present: true},
|
||||
{Slot: "BP0:1", Type: "HDD", Present: true},
|
||||
{Slot: "OB01", Type: "NVMe", Present: true},
|
||||
},
|
||||
}
|
||||
|
||||
enrichStorageFromSerialFallbackFiles(files, hw)
|
||||
|
||||
if hw.Storage[0].SerialNumber == "" || hw.Storage[1].SerialNumber == "" {
|
||||
t.Fatalf("expected serials assigned to present storage entries, got %#v", hw.Storage)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user