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:
co-authored by
Claude Sonnet 5
parent
6e1a8232ec
commit
f599215760
+60
@@ -0,0 +1,60 @@
|
||||
// Package redfishwalk is a vendor-agnostic fallback for archives that carry
|
||||
// a captured Redfish directory-tree walk (see vendors/redfishtree for the
|
||||
// detection rule) but aren't claimed by a dedicated vendor parser. Dell TSR
|
||||
// bundles from iDRAC10-generation firmware are the first known source of
|
||||
// this format, and are handled directly by vendors/dell (which also carries
|
||||
// other Dell-specific markers and merges the walk into DCIM-XML-derived
|
||||
// data). This parser exists so any other vendor that starts shipping the
|
||||
// same kind of raw Redfish walk is picked up automatically, without needing
|
||||
// a dedicated parser first.
|
||||
package redfishwalk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/collector"
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser"
|
||||
"git.mchus.pro/mchus/logpile/internal/parser/vendors/redfishtree"
|
||||
)
|
||||
|
||||
const parserVersion = "1.0"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
}
|
||||
|
||||
// Parser implements VendorParser for archives containing a raw Redfish
|
||||
// directory-tree walk with no other recognizable vendor markers.
|
||||
type Parser struct{}
|
||||
|
||||
func (p *Parser) Name() string { return "Generic Redfish Walk Parser" }
|
||||
func (p *Parser) Vendor() string { return "redfish_walk" }
|
||||
func (p *Parser) Version() string { return parserVersion }
|
||||
|
||||
// Detect returns a confidence deliberately placed above the generic text
|
||||
// fallback (15) but below any dedicated vendor parser, so a vendor-specific
|
||||
// parser always wins when both recognize the same archive.
|
||||
func (p *Parser) Detect(files []parser.ExtractedFile) int {
|
||||
for _, f := range redfishtree.FindCandidateArchives(files) {
|
||||
if redfishtree.Build(f.Content) != nil {
|
||||
return 35
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, error) {
|
||||
for _, f := range redfishtree.FindCandidateArchives(files) {
|
||||
tree := redfishtree.Build(f.Content)
|
||||
if tree == nil {
|
||||
continue
|
||||
}
|
||||
result, err := collector.ReplayRedfishFromRawPayloads(map[string]any{"redfish_tree": tree}, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("redfish_walk: no Redfish tree snapshot found")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user