fix(parser): parse Dell iDRAC10 TSR inventory from captured Redfish walk

iDRAC10-generation TSR bundles no longer ship sysinfo_dcim_view.xml /
sysinfo_dcim_softwareidentity.xml, so the dell parser produced events but
no hardware inventory for them. These bundles instead carry
redfishidracwalk.tar.gz, a captured dump of the iDRAC's own Redfish tree.

Add vendors/redfishtree, a shared helper that reconstructs a path->document
map from a tar.gz/zip-packaged Redfish walk (vendor-independent detection:
path hint + /redfish/v1 service-root/Systems/Chassis structural check) and
replays it through the existing collector.ReplayRedfishFromRawPayloads.
vendors/dell uses it to enrich DCIM-XML-derived data (append-only, existing
dedupe passes resolve overlaps). Also register vendors/redfishwalk, a
low-confidence fallback VendorParser using the same helpers, so any other
vendor that starts shipping this kind of raw Redfish walk is picked up
automatically without a dedicated parser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-11 10:49:32 +03:00
co-authored by Claude Sonnet 5
parent 6e1a8232ec
commit f599215760
9 changed files with 693 additions and 1 deletions
+92
View File
@@ -0,0 +1,92 @@
package redfishwalk
import (
"archive/tar"
"bytes"
"compress/gzip"
"testing"
"git.mchus.pro/mchus/logpile/internal/parser"
)
func makeWalkTarGz(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gzw)
for name, content := range files {
hdr := &tar.Header{Name: name, Mode: 0o644, Size: int64(len(content))}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("write tar header %s: %v", name, err)
}
if _, err := tw.Write([]byte(content)); err != nil {
t.Fatalf("write tar content %s: %v", name, err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("close tar: %v", err)
}
if err := gzw.Close(); err != nil {
t.Fatalf("close gzip: %v", err)
}
return buf.Bytes()
}
func minimalWalk(t *testing.T) []byte {
return makeWalkTarGz(t, map[string]string{
"redfish/v1/index.json": `{"@odata.id":"/redfish/v1"}`,
"redfish/v1/Systems/index.json": `{
"@odata.id":"/redfish/v1/Systems",
"Members":[{"@odata.id":"/redfish/v1/Systems/System.Embedded.1"}]
}`,
"redfish/v1/Systems/System.Embedded.1/index.json": `{
"@odata.id":"/redfish/v1/Systems/System.Embedded.1",
"Id":"System.Embedded.1",
"Manufacturer":"Acme Corp",
"Model":"Widget 9000"
}`,
})
}
func TestDetect_NoMarker(t *testing.T) {
p := &Parser{}
files := []parser.ExtractedFile{
{Path: "readme.txt", Content: []byte("hello world")},
}
if score := p.Detect(files); score != 0 {
t.Fatalf("expected 0 confidence for archive with no Redfish marker, got %d", score)
}
}
func TestDetect_NameHintWithoutStructure(t *testing.T) {
// A file whose name mentions "redfish" but whose content isn't a real
// service-tree walk must not be treated as a match.
p := &Parser{}
files := []parser.ExtractedFile{
{Path: "redfish-notes.tar.gz", Content: []byte("not a real archive")},
}
if score := p.Detect(files); score != 0 {
t.Fatalf("expected 0 confidence for non-archive content, got %d", score)
}
}
func TestDetectAndParse_GenericVendorWalk(t *testing.T) {
walk := minimalWalk(t)
p := &Parser{}
files := []parser.ExtractedFile{
{Path: "support-bundle/inventory/some_vendor_redfish_dump.tar.gz", Content: walk},
}
score := p.Detect(files)
if score <= 0 || score >= 100 {
t.Fatalf("expected low-but-positive fallback confidence, got %d", score)
}
result, err := p.Parse(files)
if err != nil {
t.Fatalf("Parse() failed: %v", err)
}
if result.Hardware == nil || result.Hardware.BoardInfo.Manufacturer != "Acme Corp" {
t.Fatalf("expected board info from replayed Redfish tree, got %+v", result.Hardware)
}
}