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>
88 lines
2.4 KiB
Go
88 lines
2.4 KiB
Go
package collector
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
// collectLicenses reads the standard DMTF LicenseService/Licenses collection
|
|
// (Redfish License.v1_x schema). Seen first on Dell iDRAC10-generation
|
|
// firmware, which exposes it alongside the legacy Oem/Dell license resources.
|
|
// Entries are system-level licenses unless AuthorizationScope is "Device",
|
|
// in which case Links.AuthorizedDevices identifies the licensed component.
|
|
func (r redfishSnapshotReader) collectLicenses() []models.License {
|
|
memberDocs, err := r.getCollectionMembers("/redfish/v1/LicenseService/Licenses")
|
|
if err != nil || len(memberDocs) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]models.License, 0, len(memberDocs))
|
|
for _, doc := range memberDocs {
|
|
lic, ok := parseRedfishLicense(doc)
|
|
if !ok {
|
|
continue
|
|
}
|
|
out = append(out, lic)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseRedfishLicense(doc map[string]interface{}) (models.License, bool) {
|
|
name := strings.TrimSpace(firstNonEmpty(
|
|
asString(doc["Description"]),
|
|
asString(doc["Name"]),
|
|
asString(doc["Id"]),
|
|
))
|
|
if name == "" {
|
|
return models.License{}, false
|
|
}
|
|
|
|
origin := strings.ToLower(strings.TrimSpace(asString(doc["LicenseOrigin"])))
|
|
present := origin == "" || origin == "installed"
|
|
|
|
lic := models.License{
|
|
Name: name,
|
|
LicenseKey: strings.TrimSpace(asString(doc["EntitlementId"])),
|
|
Type: strings.TrimSpace(asString(doc["LicenseType"])),
|
|
ComponentRef: redfishLicenseComponentRef(doc),
|
|
Present: present,
|
|
Status: mapStatus(doc["Status"]),
|
|
ActivatedAt: parseRedfishLicenseTime(doc["InstallDate"]),
|
|
ExpiresAt: parseRedfishLicenseTime(doc["ExpirationDate"]),
|
|
}
|
|
return lic, true
|
|
}
|
|
|
|
func parseRedfishLicenseTime(v interface{}) time.Time {
|
|
raw := strings.TrimSpace(asString(v))
|
|
if raw == "" {
|
|
return time.Time{}
|
|
}
|
|
for _, layout := range []string{time.RFC3339, time.RFC3339Nano} {
|
|
if ts, err := time.Parse(layout, raw); err == nil {
|
|
return ts.UTC()
|
|
}
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
func redfishLicenseComponentRef(doc map[string]interface{}) string {
|
|
if !strings.EqualFold(strings.TrimSpace(asString(doc["AuthorizationScope"])), "Device") {
|
|
return ""
|
|
}
|
|
links, ok := doc["Links"].(map[string]interface{})
|
|
if !ok {
|
|
return ""
|
|
}
|
|
devices, ok := links["AuthorizedDevices"].([]interface{})
|
|
if !ok || len(devices) == 0 {
|
|
return ""
|
|
}
|
|
first, ok := devices[0].(map[string]interface{})
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(asString(first["@odata.id"]))
|
|
}
|