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
@@ -0,0 +1,138 @@
package collector
import (
"testing"
)
func TestParseRedfishLicense(t *testing.T) {
doc := map[string]interface{}{
"@odata.id": "/redfish/v1/LicenseService/Licenses/FD00000043163704",
"Description": "iDRAC10 17G Enterprise License",
"EntitlementId": "FD00000043163704",
"LicenseType": "Production",
"LicenseOrigin": "Installed",
"AuthorizationScope": "Service",
"InstallDate": nil,
"ExpirationDate": nil,
"Links": map[string]interface{}{},
"Status": map[string]interface{}{
"Health": "OK",
"State": "Enabled",
},
}
lic, ok := parseRedfishLicense(doc)
if !ok {
t.Fatal("expected license to parse")
}
if lic.Name != "iDRAC10 17G Enterprise License" {
t.Errorf("Name = %q, want %q", lic.Name, "iDRAC10 17G Enterprise License")
}
if lic.LicenseKey != "FD00000043163704" {
t.Errorf("LicenseKey = %q, want %q", lic.LicenseKey, "FD00000043163704")
}
if lic.Type != "Production" {
t.Errorf("Type = %q, want %q", lic.Type, "Production")
}
if !lic.Present {
t.Error("expected Present = true for LicenseOrigin=Installed")
}
if lic.ComponentRef != "" {
t.Errorf("ComponentRef = %q, want empty for Service-scoped license", lic.ComponentRef)
}
if lic.Status != "OK" {
t.Errorf("Status = %q, want %q", lic.Status, "OK")
}
if !lic.ActivatedAt.IsZero() {
t.Errorf("expected zero ActivatedAt for null InstallDate, got %v", lic.ActivatedAt)
}
}
func TestParseRedfishLicense_DeviceScoped(t *testing.T) {
doc := map[string]interface{}{
"Description": "NVIDIA vGPU",
"EntitlementId": "ABC123",
"AuthorizationScope": "Device",
"LicenseOrigin": "Installed",
"InstallDate": "2025-06-01T00:00:00Z",
"ExpirationDate": "2026-12-31T23:59:59Z",
"Links": map[string]interface{}{
"AuthorizedDevices": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/0-193-0"},
},
},
"Status": map[string]interface{}{"Health": "Warning"},
}
lic, ok := parseRedfishLicense(doc)
if !ok {
t.Fatal("expected license to parse")
}
if lic.ComponentRef != "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/0-193-0" {
t.Errorf("ComponentRef = %q, want the linked device path", lic.ComponentRef)
}
if lic.Status != "Warning" {
t.Errorf("Status = %q, want %q", lic.Status, "Warning")
}
if lic.ActivatedAt.IsZero() || lic.ExpiresAt.IsZero() {
t.Errorf("expected non-zero ActivatedAt/ExpiresAt, got %v / %v", lic.ActivatedAt, lic.ExpiresAt)
}
}
func TestParseRedfishLicense_NotInstalledIsNotPresent(t *testing.T) {
doc := map[string]interface{}{
"Description": "Available Feature",
"LicenseOrigin": "NotInstalled",
}
lic, ok := parseRedfishLicense(doc)
if !ok {
t.Fatal("expected license to parse")
}
if lic.Present {
t.Error("expected Present = false for LicenseOrigin=NotInstalled")
}
}
func TestParseRedfishLicense_MissingNameSkipped(t *testing.T) {
if _, ok := parseRedfishLicense(map[string]interface{}{}); ok {
t.Error("expected license without any name field to be skipped")
}
}
func TestReplayRedfishFromRawPayloads_CollectsLicenses(t *testing.T) {
rawPayloads := map[string]any{
"redfish_tree": map[string]interface{}{
"/redfish/v1": map[string]interface{}{},
"/redfish/v1/Systems": map[string]interface{}{
"Members": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
},
},
"/redfish/v1/Systems/1": map[string]interface{}{"Id": "1"},
"/redfish/v1/LicenseService/Licenses": map[string]interface{}{
"Members": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/LicenseService/Licenses/A"},
},
},
"/redfish/v1/LicenseService/Licenses/A": map[string]interface{}{
"Description": "iDRAC10 17G Enterprise License",
"EntitlementId": "A",
"LicenseType": "Production",
"LicenseOrigin": "Installed",
"AuthorizationScope": "Service",
"Status": map[string]interface{}{"Health": "OK"},
},
},
}
result, err := ReplayRedfishFromRawPayloads(rawPayloads, nil)
if err != nil {
t.Fatalf("ReplayRedfishFromRawPayloads() failed: %v", err)
}
if len(result.Hardware.Licenses) != 1 {
t.Fatalf("expected 1 license, got %+v", result.Hardware.Licenses)
}
if result.Hardware.Licenses[0].Name != "iDRAC10 17G Enterprise License" {
t.Errorf("license name = %q, want %q", result.Hardware.Licenses[0].Name, "iDRAC10 17G Enterprise License")
}
}