Diffing an NF5280M6 BMC dump against its BEE-SP live-CD bundle found two blind spots in the combined-component.log onekeylog layout (no devicefrusdr.log / asset.json): - board manufacturer/product/part/uuid empty and stats.fru 0: the "RESTful FRU info:" JSON block was never parsed. New component_fru.go (ParseComponentLogFRU) flattens it to []models.FRUInfo, prefers the product-area system serial over the board PCB serial, and sets BoardInfo.UUID from system_uuid. Wired as a fallback only when result.FRU is still empty. - zero fan sensors: FanRESTInfo.FansPower was int but this firmware writes "fans_power": 12.000000, so json.Unmarshal of the whole fan block failed. Changed to float64. Also included: SOL smartd SCSI/SAS device-line parsing and diagnose.go gofmt from concurrent work on the same live-CD-diff task. See ADL-064. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LffAvostt3uMkiUbVUiyM
255 lines
8.3 KiB
Go
255 lines
8.3 KiB
Go
package inspur
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
const solSmartdSample = `
|
|
[ 17.219818] smartd[3321]: Device: /dev/sda [SAT], Micron_5400_MTFDDAK480TGA, S/N:2310400DC7E3, WWN:5-00a075-1400dc7e3, FW:D4CM003, 480 GB
|
|
[ 17.553024] smartd[3321]: Device: /dev/sdc [SAT], MTFDDAK3T8TGA-1BC1ZABDA, S/N:25134F172DB3, WWN:5-00a075-14f172db3, FW:D4DK403, 3.84 TB
|
|
[ 17.553331] smartd[3321]: Device: /dev/sde [SAT], Micron_5400_MTFDDAK480TGA, S/N:2310400DC80F, WWN:5-00a075-1400dc80f, FW:D4CM003, 480 GB
|
|
[ 17.553709] smartd[3321]: Device: /dev/sdh [SAT], MTFDDAK3T8TGA-1BC1ZABDA, S/N:25134F57DAB8, WWN:5-00a075-14f57dab8, FW:D4DK403, 3.84 TB
|
|
[ 17.886180] smartd[3321]: Device: /dev/sda [SAT], state written to /var/lib/smartmontools/smartd.Micron-2310400DC7E3.ata.state
|
|
`
|
|
|
|
func TestParseSOLSmartdDevices_Dedup(t *testing.T) {
|
|
devices := parseSOLSmartdDevices([]byte(solSmartdSample))
|
|
if len(devices) != 4 {
|
|
t.Fatalf("expected 4 unique devices, got %d: %v", len(devices), devices)
|
|
}
|
|
// order matches first-seen
|
|
if devices[0].Serial != "2310400DC7E3" {
|
|
t.Errorf("first device serial: got %q, want 2310400DC7E3", devices[0].Serial)
|
|
}
|
|
if devices[0].SizeGB != 480 {
|
|
t.Errorf("first device size: got %d, want 480", devices[0].SizeGB)
|
|
}
|
|
if devices[1].SizeGB != 3840 {
|
|
t.Errorf("TB device size: got %d, want 3840", devices[1].SizeGB)
|
|
}
|
|
if devices[1].Firmware != "D4DK403" {
|
|
t.Errorf("firmware: got %q, want D4DK403", devices[1].Firmware)
|
|
}
|
|
}
|
|
|
|
// SAS drives (and SATA drives behind a SAS HBA in SCSI mode) print a different
|
|
// smartd line shape: no [SAT] tag, a bracketed SCSI INQUIRY triple, "lu id",
|
|
// spaced "S/N: ", and no firmware field.
|
|
const solSmartdSCSISample = `
|
|
[ 21.481033] smartd[2015]: Device: /dev/sdb, [SEAGATE ST6000NM005B K0A1], lu id: 0x5000c500ee6e11f7, S/N: WX004FCC0000E22967PY, 6.00 TB
|
|
[ 21.481212] smartd[2015]: Device: /dev/sdd, [SEAGATE ST6000NM005B K0A1], lu id: 0x5000c500ee6f5ebf, S/N: WX004TKE0000E233A32A, 6.00 TB
|
|
[ 21.481235] smartd[2015]: Device: /dev/sdd, is SMART capable. Adding to "monitor" list.
|
|
`
|
|
|
|
func TestParseSOLSmartdDevices_SCSI(t *testing.T) {
|
|
devices := parseSOLSmartdDevices([]byte(solSmartdSCSISample))
|
|
if len(devices) != 2 {
|
|
t.Fatalf("expected 2 SCSI devices, got %d: %v", len(devices), devices)
|
|
}
|
|
d := devices[0]
|
|
if d.Serial != "WX004FCC0000E22967PY" {
|
|
t.Errorf("serial: got %q", d.Serial)
|
|
}
|
|
if d.Model != "SEAGATE ST6000NM005B" {
|
|
t.Errorf("model: got %q, want %q", d.Model, "SEAGATE ST6000NM005B")
|
|
}
|
|
if d.SizeGB != 6000 {
|
|
t.Errorf("size: got %d, want 6000", d.SizeGB)
|
|
}
|
|
if d.Interface != "SAS" {
|
|
t.Errorf("interface: got %q, want SAS", d.Interface)
|
|
}
|
|
if d.Firmware != "" {
|
|
t.Errorf("firmware: got %q, want empty (not reported for SCSI)", d.Firmware)
|
|
}
|
|
}
|
|
|
|
func TestParseSOLSmartdDevices_MixedSATAandSCSI(t *testing.T) {
|
|
devices := parseSOLSmartdDevices([]byte(solSmartdSample + solSmartdSCSISample))
|
|
if len(devices) != 6 {
|
|
t.Fatalf("expected 4 SAT + 2 SCSI = 6 devices, got %d", len(devices))
|
|
}
|
|
got := map[string]string{}
|
|
for _, d := range devices {
|
|
got[d.Serial] = d.Interface
|
|
}
|
|
if got["2310400DC7E3"] != "SATA" {
|
|
t.Errorf("SAT device interface: got %q", got["2310400DC7E3"])
|
|
}
|
|
if got["WX004FCC0000E22967PY"] != "SAS" {
|
|
t.Errorf("SCSI device interface: got %q", got["WX004FCC0000E22967PY"])
|
|
}
|
|
}
|
|
|
|
func TestScsiInquiryModel(t *testing.T) {
|
|
cases := []struct{ raw, want string }{
|
|
{"SEAGATE ST6000NM005B K0A1", "SEAGATE ST6000NM005B"},
|
|
{"ATA Micron_5400_MTFD K0A1", "Micron_5400_MTFD"},
|
|
{"SEAGATE ST6000NM005B", "SEAGATE ST6000NM005B"},
|
|
{"BareModel", "BareModel"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := scsiInquiryModel(c.raw); got != c.want {
|
|
t.Errorf("scsiInquiryModel(%q) = %q, want %q", c.raw, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseSOLSmartdDevices_SkipsNonInfoLines(t *testing.T) {
|
|
content := `
|
|
[ 17.886177] smartd[3321]: Device: /dev/sda [SAT], state written to /var/lib/smartmontools/smartd.foo.ata.state
|
|
[ 17.040843] smartd[3321]: Device: /dev/sda [SAT], not found in smartd database 7.3/5319.
|
|
[ 17.040865] smartd[3321]: Device: /dev/sda [SAT], is SMART capable. Adding to "monitor" list.
|
|
`
|
|
devices := parseSOLSmartdDevices([]byte(content))
|
|
if len(devices) != 0 {
|
|
t.Errorf("expected 0 devices, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
func TestParseSolSizeGB(t *testing.T) {
|
|
cases := []struct {
|
|
value, unit string
|
|
want int
|
|
}{
|
|
{"480", "GB", 480},
|
|
{"1.92", "TB", 1920},
|
|
{"3.84", "TB", 3840},
|
|
{"1", "TB", 1000},
|
|
{"0", "GB", 0},
|
|
}
|
|
for _, c := range cases {
|
|
got := parseSolSizeGB(c.value, c.unit)
|
|
if got != c.want {
|
|
t.Errorf("parseSolSizeGB(%q, %q) = %d, want %d", c.value, c.unit, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSolStorageType(t *testing.T) {
|
|
cases := []struct {
|
|
model string
|
|
want string
|
|
}{
|
|
{"MTFDDAK3T8TGA-1BC1ZABDA", "SSD"},
|
|
{"Micron_5400_MTFDDAK480TGA", "SSD"},
|
|
{"INTEL SSDSC2KB019TZ", "SSD"},
|
|
{"SEAGATE ST4000NM0115", "HDD"},
|
|
}
|
|
for _, c := range cases {
|
|
got := solStorageType(c.model)
|
|
if got != c.want {
|
|
t.Errorf("solStorageType(%q) = %q, want %q", c.model, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichStorageFromSOLSmartd_ModelMatch(t *testing.T) {
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "onekeylog/log/sollog/SOLHostCapture.log",
|
|
Content: []byte(solSmartdSample),
|
|
},
|
|
}
|
|
hw := &models.HardwareConfig{
|
|
Storage: []models.Storage{
|
|
{Slot: "BP0:0", Model: "MTFDDAK3T8TGA-1BC1ZABDA", SizeGB: 3576, Present: true},
|
|
{Slot: "BP0:1", Model: "MTFDDAK3T8TGA-1BC1ZABDA", SizeGB: 3576, Present: true},
|
|
},
|
|
}
|
|
|
|
enrichStorageFromSOLSmartd(files, hw)
|
|
|
|
// The two existing slots must have received serials via model match.
|
|
for _, s := range hw.Storage[:2] {
|
|
if s.SerialNumber == "" {
|
|
t.Errorf("slot %q: expected serial to be assigned via model match", s.Slot)
|
|
}
|
|
if s.SizeGB != 3576 {
|
|
t.Errorf("slot %q: size should be preserved, got %d", s.Slot, s.SizeGB)
|
|
}
|
|
}
|
|
// The two unmatched Micron entries should be added as new storage entries.
|
|
if len(hw.Storage) != 4 {
|
|
t.Errorf("expected 4 total storage entries (2 existing + 2 new Micron), got %d", len(hw.Storage))
|
|
}
|
|
}
|
|
|
|
func TestEnrichStorageFromSOLSmartd_PlaceholderSlots(t *testing.T) {
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "onekeylog/log/sollog/SOLHostCapture.log",
|
|
Content: []byte(solSmartdSample),
|
|
},
|
|
}
|
|
hw := &models.HardwareConfig{
|
|
Storage: []models.Storage{
|
|
{Slot: "BP0:0", Present: true},
|
|
{Slot: "BP0:1", Present: true},
|
|
},
|
|
}
|
|
|
|
enrichStorageFromSOLSmartd(files, hw)
|
|
|
|
for _, s := range hw.Storage {
|
|
if s.SerialNumber == "" {
|
|
t.Errorf("slot %q: expected serial to be assigned", s.Slot)
|
|
}
|
|
if s.Model == "" {
|
|
t.Errorf("slot %q: expected model to be assigned", s.Slot)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnrichStorageFromSOLSmartd_SkipsExistingSerial(t *testing.T) {
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "onekeylog/log/sollog/SOLHostCapture.log",
|
|
Content: []byte(solSmartdSample),
|
|
},
|
|
}
|
|
hw := &models.HardwareConfig{
|
|
Storage: []models.Storage{
|
|
{Slot: "BP0:0", SerialNumber: "2310400DC7E3", Present: true},
|
|
},
|
|
}
|
|
before := len(hw.Storage)
|
|
|
|
enrichStorageFromSOLSmartd(files, hw)
|
|
|
|
// BP0:0 should still have original serial unchanged
|
|
if hw.Storage[0].SerialNumber != "2310400DC7E3" {
|
|
t.Errorf("existing serial was changed: got %q", hw.Storage[0].SerialNumber)
|
|
}
|
|
// Remaining 3 devices should be added as new entries
|
|
if len(hw.Storage) <= before {
|
|
t.Errorf("expected new entries to be added, got %d (same as before)", len(hw.Storage))
|
|
}
|
|
}
|
|
|
|
func TestEnrichStorageFromSOLSmartd_MergesTwoFiles(t *testing.T) {
|
|
// Two SOL files with partial overlap; combined unique serials = 3
|
|
file1 := `[ 17.0] smartd[1]: Device: /dev/sda [SAT], ModelA, S/N:SN001, WWN:w, FW:fw1, 480 GB`
|
|
file2 := strings.Join([]string{
|
|
`[ 17.0] smartd[2]: Device: /dev/sda [SAT], ModelA, S/N:SN001, WWN:w, FW:fw1, 480 GB`,
|
|
`[ 17.1] smartd[2]: Device: /dev/sdb [SAT], ModelB, S/N:SN002, WWN:w, FW:fw2, 480 GB`,
|
|
`[ 17.2] smartd[2]: Device: /dev/sdc [SAT], ModelC, S/N:SN003, WWN:w, FW:fw3, 480 GB`,
|
|
}, "\n")
|
|
|
|
files := []parser.ExtractedFile{
|
|
{Path: "log/sollog/SOLHostCapture.log", Content: []byte(file1)},
|
|
{Path: "runningdata/var/sollog/SOLHostCapture.log", Content: []byte(file2)},
|
|
}
|
|
hw := &models.HardwareConfig{}
|
|
|
|
enrichStorageFromSOLSmartd(files, hw)
|
|
|
|
if len(hw.Storage) != 3 {
|
|
t.Fatalf("expected 3 unique storage entries, got %d", len(hw.Storage))
|
|
}
|
|
}
|