feat(hardware): collect and export licenses via Dell iDRAC10 Redfish walk

Implements the hardware.licenses[] contract section (v2.12, refreshed from
reanimator/core's hardware-ingest-contract.md — was v2.11 locally).

- models.License / HardwareConfig.Licenses mirror the contract field set.
- collector.collectLicenses() reads the standard DMTF
  /redfish/v1/LicenseService/Licenses collection during Redfish-walk replay;
  it's a generic DMTF resource, not Dell-specific, so any future vendor's
  Redfish walk gets license collection for free through
  ReplayRedfishFromRawPayloads.
- vendors/dell merges replayed Licenses like every other category.
- exporter.convertLicenses/dedupeLicenses wire hw.Licenses into the
  reanimator export directly (no canonical-devices merge — licenses have no
  physical identity to merge on), setting Present on every record from the
  start (per the ADL-049 round-trip lesson).
- chart viewer renders a licenses section in /chart/current.

Verified end-to-end on the PowerEdge R7715 (1TVFYL4) TSR: 3 system-level
licenses extracted and correctly exported/rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-11 11:48:21 +03:00
co-authored by Claude Sonnet 5
parent eb6cc207ce
commit eaaf5c09d3
13 changed files with 582 additions and 10 deletions
@@ -2077,3 +2077,75 @@ func TestConvertToReanimator_MemoryAndPSURoundTripSurvivesReimport(t *testing.T)
t.Fatalf("power supplies did not survive reanimator round trip, got %+v", reconverted.Hardware.PowerSupplies)
}
}
// TestConvertToReanimator_ExportsLicenses covers the hardware.licenses contract
// section (v2.12): system-level licenses (no component_ref) and component-scoped
// licenses, skipping records without a name and records the source marked absent.
func TestConvertToReanimator_ExportsLicenses(t *testing.T) {
activatedAt := time.Date(2025, 1, 10, 0, 0, 0, 0, time.UTC)
result := &models.AnalysisResult{
Filename: "test.zip",
CollectedAt: time.Date(2026, 8, 11, 8, 42, 18, 0, time.UTC),
Hardware: &models.HardwareConfig{
BoardInfo: models.BoardInfo{
Manufacturer: "Dell Inc.",
ProductName: "PowerEdge R7715",
SerialNumber: "1TVFYL4",
},
Licenses: []models.License{
{
Name: "iDRAC10 17G Enterprise License",
LicenseKey: "FD00000043163704",
Type: "Production",
Present: true,
Status: "OK",
ActivatedAt: activatedAt,
},
{
Name: "NVIDIA vGPU",
ComponentRef: "0000:3b:00.0",
Present: true,
Status: "Warning",
},
{
// No name: source didn't provide one, must not be synthesized/kept.
LicenseKey: "NONAME",
Present: true,
Status: "OK",
},
{
Name: "Not Actually Installed",
Present: false,
Status: "OK",
},
},
},
}
exported, err := ConvertToReanimator(result)
if err != nil {
t.Fatalf("ConvertToReanimator() failed: %v", err)
}
if len(exported.Hardware.Licenses) != 2 {
t.Fatalf("expected 2 licenses (nameless and not-present filtered out), got %+v", exported.Hardware.Licenses)
}
system := exported.Hardware.Licenses[0]
if system.Name != "iDRAC10 17G Enterprise License" || system.ComponentRef != "" {
t.Errorf("system license = %+v, want system-level iDRAC10 entry", system)
}
if system.Present == nil || !*system.Present {
t.Errorf("expected system license present=true, got %+v", system.Present)
}
if system.ActivatedAt != "2025-01-10T00:00:00Z" {
t.Errorf("ActivatedAt = %q, want %q", system.ActivatedAt, "2025-01-10T00:00:00Z")
}
scoped := exported.Hardware.Licenses[1]
if scoped.Name != "NVIDIA vGPU" || scoped.ComponentRef != "0000:3b:00.0" {
t.Errorf("component license = %+v, want vGPU entry scoped to 0000:3b:00.0", scoped)
}
if scoped.Status != "Warning" {
t.Errorf("Status = %q, want %q", scoped.Status, "Warning")
}
}