diff --git a/internal/parser/vendors/xfusion/hardware.go b/internal/parser/vendors/xfusion/hardware.go index 2858bd0..199c0c1 100644 --- a/internal/parser/vendors/xfusion/hardware.go +++ b/internal/parser/vendors/xfusion/hardware.go @@ -2,6 +2,7 @@ package xfusion import ( "fmt" + "math" "strconv" "strings" "time" @@ -1086,11 +1087,7 @@ func parseDiskInfo(content []byte) *models.Storage { return nil } - sizeGB := 0 - var capFloat float64 - if _, err := fmt.Sscanf(fields["Capacity"], "%f GB", &capFloat); err == nil { - sizeGB = int(capFloat) - } + sizeGB := parseDiskCapacityGB(fields["Capacity"]) var wearPct *int if wearStr := fields["Remnant Media Wearout"]; wearStr != "" { @@ -1120,6 +1117,34 @@ func parseDiskInfo(content []byte) *models.Storage { } } +// parseDiskCapacityGB converts an iBMC disk_info "Capacity" value to decimal GB. +// iBMC labels the number "GB"/"TB" but the value is binary (GiB/TiB): a +// "6.986 TB" NVMe drive is the vendor's 7.68 TB / 7680 GB part, which is also +// what the BEE-SP live-CD inventory reports. Normalize to decimal GB so a +// BMC-dump and a live-CD export of the same drive carry the same size_gb. +func parseDiskCapacityGB(s string) int { + f := strings.Fields(strings.TrimSpace(s)) + if len(f) < 2 { + return 0 + } + val, err := strconv.ParseFloat(f[0], 64) + if err != nil || val <= 0 { + return 0 + } + var binaryBytes float64 + switch strings.ToUpper(f[1]) { + case "TB", "TIB": + binaryBytes = val * (1 << 40) + case "GB", "GIB": + binaryBytes = val * (1 << 30) + case "MB", "MIB": + binaryBytes = val * (1 << 20) + default: + return 0 + } + return int(math.Round(binaryBytes / 1e9)) +} + // parseKeyValueBlock parses "Key (spaces) : Value" lines from a text block. func parseKeyValueBlock(content []byte) map[string]string { result := make(map[string]string) diff --git a/internal/parser/vendors/xfusion/parser_test.go b/internal/parser/vendors/xfusion/parser_test.go index 8df2db9..533960e 100644 --- a/internal/parser/vendors/xfusion/parser_test.go +++ b/internal/parser/vendors/xfusion/parser_test.go @@ -369,3 +369,55 @@ func TestParseMemInfo_EmbeddedNewlineInBOM(t *testing.T) { t.Errorf("m170 fields not recovered: %+v", m170) } } + +// TestParseDiskInfo_CapacityUnits guards the disk_info "Capacity" parse: iBMC +// labels the number "GB"/"TB" but the value is binary, and the old code only +// matched a literal " GB" so every "X TB" NVMe drive got size_gb 0 while the +// BEE-SP live-CD export of the same drive reported the real decimal size. +func TestParseDiskInfo_CapacityUnits(t *testing.T) { + cases := []struct { + name string + content string + wantGB int + }{ + { + name: "kioxia nvme reported as TB", + content: "Serial Number : 9F30A0440V43\n" + + "Model : KIOXIA KCD8XPUG7T68\n" + + "Media Type : SSD\n" + + "Interface Type : PCIe\n" + + "Capacity : 6.986 TB\n", + wantGB: 7680, + }, + { + name: "intel sata reported as TB", + content: "Serial Number : PHYF202100WK3P8EGN\n" + + "Model : INTEL SSDSC2KB038T8\n" + + "Capacity : 3.492 TB\n", + wantGB: 3840, + }, + { + name: "boot ssd reported as GB", + content: "Serial Number : X1\nModel : M\nCapacity : 446.625 GB\n", + wantGB: 480, + }, + { + name: "missing capacity", + content: "Serial Number : X2\nModel : M\n", + wantGB: 0, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + d := parseDiskInfo([]byte(tc.content)) + if d == nil { + t.Fatal("parseDiskInfo returned nil") + } + // iBMC reports binary units; a few GB of rounding slack vs the + // live-CD's exact byte count is acceptable (size_gb is display-only). + if diff := d.SizeGB - tc.wantGB; diff < -2 || diff > 2 { + t.Errorf("SizeGB = %d, want ~%d", d.SizeGB, tc.wantGB) + } + }) + } +}