export: align reanimator contract v2.7

This commit is contained in:
Mikhail Chusavitin
2026-03-15 23:27:32 +03:00
parent 9007f1b360
commit 476630190d
31 changed files with 3502 additions and 689 deletions

View File

@@ -69,6 +69,32 @@ func (c *RedfishConnector) Protocol() string {
return "redfish"
}
func (c *RedfishConnector) Probe(ctx context.Context, req Request) (*ProbeResult, error) {
baseURL, err := c.baseURL(req)
if err != nil {
return nil, err
}
client := c.httpClientWithTimeout(req, c.timeout)
if _, err := c.getJSON(ctx, client, req, baseURL, "/redfish/v1"); err != nil {
return nil, fmt.Errorf("redfish service root: %w", err)
}
systemPaths := c.discoverMemberPaths(ctx, client, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
systemDoc, err := c.getJSON(ctx, client, req, baseURL, primarySystem)
if err != nil {
return nil, fmt.Errorf("redfish system: %w", err)
}
powerState := strings.TrimSpace(asString(systemDoc["PowerState"]))
return &ProbeResult{
Reachable: true,
Protocol: "redfish",
HostPowerState: powerState,
HostPoweredOn: isRedfishHostPoweredOn(powerState),
PowerControlAvailable: redfishResetActionTarget(systemDoc) != "",
SystemPath: primarySystem,
}, nil
}
func (c *RedfishConnector) debugf(format string, args ...interface{}) {
if !c.debug {
return
@@ -102,6 +128,21 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
}
systemPaths := c.discoverMemberPaths(ctx, snapshotClient, req, baseURL, "/redfish/v1/Systems", "/redfish/v1/Systems/1")
primarySystem := firstNonEmptyPath(systemPaths, "/redfish/v1/Systems/1")
poweredOnByCollector := false
if primarySystem != "" {
if on, changed := c.ensureHostPowerForCollection(ctx, snapshotClient, req, baseURL, primarySystem, emit); on {
poweredOnByCollector = changed
}
}
defer func() {
if !poweredOnByCollector || primarySystem == "" {
return
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
c.restoreHostPowerAfterCollection(shutdownCtx, snapshotClient, req, baseURL, primarySystem, emit)
}()
chassisPaths := c.discoverMemberPaths(ctx, snapshotClient, req, baseURL, "/redfish/v1/Chassis", "/redfish/v1/Chassis/1")
managerPaths := c.discoverMemberPaths(ctx, snapshotClient, req, baseURL, "/redfish/v1/Managers", "/redfish/v1/Managers/1")
criticalPaths := redfishCriticalEndpoints(systemPaths, chassisPaths, managerPaths)
@@ -186,6 +227,257 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
return result, nil
}
func (c *RedfishConnector) ensureHostPowerForCollection(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, emit ProgressFn) (hostOn bool, poweredOnByCollector bool) {
systemDoc, err := c.getJSON(ctx, client, req, baseURL, systemPath)
if err != nil {
if emit != nil {
emit(Progress{Status: "running", Progress: 18, Message: "Redfish: не удалось проверить PowerState host, сбор продолжается без power-control"})
}
return false, false
}
powerState := strings.TrimSpace(asString(systemDoc["PowerState"]))
if isRedfishHostPoweredOn(powerState) {
if emit != nil {
emit(Progress{Status: "running", Progress: 18, Message: fmt.Sprintf("Redfish: host включен (%s)", firstNonEmpty(powerState, "On"))})
}
return true, false
}
if emit != nil {
emit(Progress{Status: "running", Progress: 18, Message: fmt.Sprintf("Redfish: host выключен (%s)", firstNonEmpty(powerState, "Off"))})
}
if !req.PowerOnIfHostOff {
if emit != nil {
emit(Progress{Status: "running", Progress: 19, Message: "Redfish: включение host не запрошено, сбор продолжается на выключенном host"})
}
return false, false
}
resetTarget := redfishResetActionTarget(systemDoc)
resetType := redfishPickResetType(systemDoc, "On", "ForceOn")
if resetTarget == "" || resetType == "" {
if emit != nil {
emit(Progress{Status: "running", Progress: 19, Message: "Redfish: action ComputerSystem.Reset недоступен, сбор продолжается на выключенном host"})
}
return false, false
}
waitWindows := []time.Duration{5 * time.Second, 10 * time.Second, 30 * time.Second}
for i, waitFor := range waitWindows {
if emit != nil {
emit(Progress{Status: "running", Progress: 19, Message: fmt.Sprintf("Redfish: попытка включения host (%d/%d), ожидание %s", i+1, len(waitWindows), waitFor)})
}
if err := c.postJSON(ctx, client, req, baseURL, resetTarget, map[string]any{"ResetType": resetType}); err != nil {
if emit != nil {
emit(Progress{Status: "running", Progress: 19, Message: fmt.Sprintf("Redfish: включение host не удалось (%v)", err)})
}
continue
}
if c.waitForHostPowerState(ctx, client, req, baseURL, systemPath, true, waitFor) {
if !c.waitForStablePoweredOnHost(ctx, client, req, baseURL, systemPath, emit) {
if emit != nil {
emit(Progress{Status: "running", Progress: 20, Message: "Redfish: host включился, но не подтвердил стабильное состояние; сбор продолжается на выключенном host"})
}
return false, false
}
if emit != nil {
emit(Progress{Status: "running", Progress: 20, Message: "Redfish: host успешно включен и стабилен перед сбором"})
}
return true, true
}
if emit != nil {
emit(Progress{Status: "running", Progress: 20, Message: fmt.Sprintf("Redfish: host не включился за %s", waitFor)})
}
}
if emit != nil {
emit(Progress{Status: "running", Progress: 20, Message: "Redfish: host не удалось включить, сбор продолжается на выключенном host"})
}
return false, false
}
func (c *RedfishConnector) waitForStablePoweredOnHost(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, emit ProgressFn) bool {
stabilizationDelay := redfishPowerOnStabilizationDelay()
if stabilizationDelay > 0 {
if emit != nil {
emit(Progress{
Status: "running",
Progress: 20,
Message: fmt.Sprintf("Redfish: host включен, ожидание стабилизации %s перед началом сбора", stabilizationDelay),
})
}
timer := time.NewTimer(stabilizationDelay)
defer timer.Stop()
select {
case <-ctx.Done():
return false
case <-timer.C:
}
}
if emit != nil {
emit(Progress{
Status: "running",
Progress: 20,
Message: "Redfish: повторная проверка PowerState после стабилизации host",
})
}
return c.waitForHostPowerState(ctx, client, req, baseURL, systemPath, true, 5*time.Second)
}
func (c *RedfishConnector) restoreHostPowerAfterCollection(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, emit ProgressFn) {
systemDoc, err := c.getJSON(ctx, client, req, baseURL, systemPath)
if err != nil {
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: "Redfish: не удалось повторно прочитать system перед выключением host"})
}
return
}
resetTarget := redfishResetActionTarget(systemDoc)
resetType := redfishPickResetType(systemDoc, "GracefulShutdown", "ForceOff", "PushPowerButton")
if resetTarget == "" || resetType == "" {
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: "Redfish: выключение host после сбора недоступно"})
}
return
}
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: "Redfish: выключаем host после завершения сбора"})
}
if err := c.postJSON(ctx, client, req, baseURL, resetTarget, map[string]any{"ResetType": resetType}); err != nil {
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: fmt.Sprintf("Redfish: не удалось выключить host после сбора (%v)", err)})
}
return
}
if c.waitForHostPowerState(ctx, client, req, baseURL, systemPath, false, 20*time.Second) {
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: "Redfish: host выключен после завершения сбора"})
}
return
}
if emit != nil {
emit(Progress{Status: "running", Progress: 100, Message: "Redfish: не удалось подтвердить выключение host после сбора"})
}
}
func (c *RedfishConnector) waitForHostPowerState(ctx context.Context, client *http.Client, req Request, baseURL, systemPath string, wantOn bool, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for {
systemDoc, err := c.getJSON(ctx, client, req, baseURL, systemPath)
if err == nil {
if isRedfishHostPoweredOn(strings.TrimSpace(asString(systemDoc["PowerState"]))) == wantOn {
return true
}
}
if time.Now().After(deadline) {
return false
}
select {
case <-ctx.Done():
return false
case <-time.After(1 * time.Second):
}
}
}
func firstNonEmptyPath(paths []string, fallback string) string {
for _, p := range paths {
if strings.TrimSpace(p) != "" {
return p
}
}
return fallback
}
func isRedfishHostPoweredOn(state string) bool {
switch strings.ToLower(strings.TrimSpace(state)) {
case "on", "poweringon":
return true
default:
return false
}
}
func redfishResetActionTarget(systemDoc map[string]interface{}) string {
if systemDoc == nil {
return ""
}
actions, _ := systemDoc["Actions"].(map[string]interface{})
reset, _ := actions["#ComputerSystem.Reset"].(map[string]interface{})
target := strings.TrimSpace(asString(reset["target"]))
if target != "" {
return target
}
odataID := strings.TrimSpace(asString(systemDoc["@odata.id"]))
if odataID == "" {
return ""
}
return joinPath(odataID, "/Actions/ComputerSystem.Reset")
}
func redfishPickResetType(systemDoc map[string]interface{}, preferred ...string) string {
actions, _ := systemDoc["Actions"].(map[string]interface{})
reset, _ := actions["#ComputerSystem.Reset"].(map[string]interface{})
allowedRaw, _ := reset["ResetType@Redfish.AllowableValues"].([]interface{})
if len(allowedRaw) == 0 {
if len(preferred) > 0 {
return preferred[0]
}
return ""
}
allowed := make([]string, 0, len(allowedRaw))
for _, item := range allowedRaw {
if v := strings.TrimSpace(asString(item)); v != "" {
allowed = append(allowed, v)
}
}
for _, want := range preferred {
for _, have := range allowed {
if strings.EqualFold(want, have) {
return have
}
}
}
return ""
}
func (c *RedfishConnector) postJSON(ctx context.Context, client *http.Client, req Request, baseURL, resourcePath string, payload map[string]any) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
target := strings.TrimSpace(resourcePath)
if !strings.HasPrefix(strings.ToLower(target), "http://") && !strings.HasPrefix(strings.ToLower(target), "https://") {
target = baseURL + normalizeRedfishPath(target)
}
u, err := url.Parse(target)
if err != nil {
return err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(string(body)))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/json")
switch req.AuthType {
case "password":
httpReq.SetBasicAuth(req.Username, req.Password)
case "token":
httpReq.Header.Set("Authorization", "Bearer "+req.Token)
}
resp, err := client.Do(httpReq)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("status %d from %s: %s", resp.StatusCode, resourcePath, strings.TrimSpace(string(respBody)))
}
return nil
}
func (c *RedfishConnector) prefetchCriticalRedfishDocs(
ctx context.Context,
client *http.Client,
@@ -417,11 +709,17 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, driveCollectionPath)
if err == nil {
for _, driveDoc := range driveDocs {
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
if len(driveDocs) == 0 {
for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, driveCollectionPath) {
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
@@ -444,6 +742,9 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
if err != nil {
continue
}
if isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
@@ -452,6 +753,9 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
// Some implementations return drive fields right in storage member object.
if looksLikeDrive(member) {
if isVirtualStorageDrive(member) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
}
@@ -462,13 +766,16 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives"))
if err == nil {
for _, driveDoc := range driveDocs {
if looksLikeDrive(driveDoc) {
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
}
if len(driveDocs) == 0 {
for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives")) {
if isVirtualStorageDrive(driveDoc) {
continue
}
out = append(out, parseDrive(driveDoc))
}
}
@@ -481,7 +788,7 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
"/Storage/IntelVROC/Drives",
"/Storage/IntelVROC/Controllers/1/Drives",
}) {
if looksLikeDrive(driveDoc) {
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
}
@@ -496,7 +803,7 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
}
for _, devAny := range devices {
devDoc, ok := devAny.(map[string]interface{})
if !ok || !looksLikeDrive(devDoc) {
if !ok || !looksLikeDrive(devDoc) || isVirtualStorageDrive(devDoc) {
continue
}
out = append(out, parseDriveWithSupplementalDocs(devDoc))
@@ -511,7 +818,7 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
continue
}
for _, driveDoc := range driveDocs {
if !looksLikeDrive(driveDoc) {
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
@@ -523,7 +830,7 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
continue
}
for _, driveDoc := range c.probeSupermicroNVMeDiskBays(ctx, client, req, baseURL, chassisPath) {
if !looksLikeDrive(driveDoc) {
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
@@ -773,6 +1080,7 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
continue
}
supplementalDocs := c.getLinkedSupplementalDocs(ctx, client, req, baseURL, doc, "EnvironmentMetrics", "Metrics")
supplementalDocs = append(supplementalDocs, c.getChassisScopedPCIeSupplementalDocs(ctx, client, req, baseURL, doc)...)
for _, fn := range functionDocs {
supplementalDocs = append(supplementalDocs, c.getLinkedSupplementalDocs(ctx, client, req, baseURL, fn, "EnvironmentMetrics", "Metrics")...)
}
@@ -800,6 +1108,39 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
return dedupePCIeDevices(out)
}
func (c *RedfishConnector) getChassisScopedPCIeSupplementalDocs(ctx context.Context, client *http.Client, req Request, baseURL string, doc map[string]interface{}) []map[string]interface{} {
if !looksLikeNVSwitchPCIeDoc(doc) {
return nil
}
docPath := normalizeRedfishPath(asString(doc["@odata.id"]))
chassisPath := chassisPathForPCIeDoc(docPath)
if chassisPath == "" {
return nil
}
out := make([]map[string]interface{}, 0, 4)
seen := make(map[string]struct{})
add := func(path string) {
path = normalizeRedfishPath(path)
if path == "" {
return
}
if _, ok := seen[path]; ok {
return
}
supplementalDoc, err := c.getJSON(ctx, client, req, baseURL, path)
if err != nil || len(supplementalDoc) == 0 {
return
}
seen[path] = struct{}{}
out = append(out, supplementalDoc)
}
add(joinPath(chassisPath, "/EnvironmentMetrics"))
add(joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"))
return out
}
func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http.Client, req Request, baseURL, collectionPath, fallbackPath string) []string {
collection, err := c.getJSON(ctx, client, req, baseURL, collectionPath)
if err == nil {
@@ -1810,6 +2151,15 @@ func redfishCriticalSlowGap() time.Duration {
return 1200 * time.Millisecond
}
func redfishPowerOnStabilizationDelay() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_POWERON_STABILIZATION")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d >= 0 {
return d
}
}
return 60 * time.Second
}
func redfishSnapshotMemoryRequestTimeout() time.Duration {
if v := strings.TrimSpace(os.Getenv("LOGPILE_REDFISH_MEMORY_TIMEOUT")); v != "" {
if d, err := time.ParseDuration(v); err == nil && d > 0 {
@@ -1984,11 +2334,22 @@ func looksLikeCanonicalBDF(bdf string) bool {
return false
}
func sanitizeRedfishBDF(bdf string) string {
bdf = strings.TrimSpace(bdf)
if looksLikeCanonicalBDF(bdf) {
return bdf
}
return ""
}
func shouldCrawlPath(path string) bool {
if path == "" {
return false
}
normalized := normalizeRedfishPath(path)
if isAllowedNVSwitchFabricPath(normalized) {
return true
}
if strings.Contains(normalized, "/Chassis/") &&
strings.Contains(normalized, "/PCIeDevices/") &&
strings.Contains(normalized, "/PCIeFunctions/") {
@@ -2079,6 +2440,22 @@ func shouldCrawlPath(path string) bool {
return true
}
func isAllowedNVSwitchFabricPath(path string) bool {
if !strings.HasPrefix(path, "/redfish/v1/Fabrics/") {
return false
}
if !strings.Contains(path, "/Switches/NVSwitch_") {
return false
}
if strings.HasSuffix(path, "/Switches") || strings.Contains(path, "/Ports/NVLink_") || strings.HasSuffix(path, "/Ports") {
return true
}
if strings.Contains(path, "/Switches/NVSwitch_") && !strings.Contains(path, "/Ports/") {
return true
}
return false
}
func isRedfishMemoryMemberPath(path string) bool {
normalized := normalizeRedfishPath(path)
if !strings.Contains(normalized, "/Systems/") {
@@ -2880,23 +3257,38 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
vendor = pciids.VendorName(vendorID)
}
location := redfishLocationLabel(doc["Location"])
slot := firstNonEmpty(redfishLocationLabel(doc["Location"]), asString(doc["Id"]), asString(doc["Name"]))
var firmware string
var portCount int
var linkWidth int
var maxLinkWidth int
var linkSpeed string
var maxLinkSpeed string
if controllers, ok := doc["Controllers"].([]interface{}); ok && len(controllers) > 0 {
if ctrl, ok := controllers[0].(map[string]interface{}); ok {
location = firstNonEmpty(location, redfishLocationLabel(ctrl["Location"]))
ctrlLocation := redfishLocationLabel(ctrl["Location"])
location = firstNonEmpty(location, ctrlLocation)
if isWeakRedfishNICSlotLabel(slot) {
slot = firstNonEmpty(ctrlLocation, slot)
}
firmware = asString(ctrl["FirmwarePackageVersion"])
if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok {
portCount = sanitizeNetworkPortCount(asInt(caps["NetworkPortCount"]))
}
if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok {
linkWidth = asInt(pcieIf["LanesInUse"])
maxLinkWidth = asInt(pcieIf["MaxLanes"])
linkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"]))
maxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"]))
}
}
}
return models.NetworkAdapter{
Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])),
Slot: slot,
Location: location,
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
BDF: asString(doc["BDF"]),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
Model: strings.TrimSpace(model),
Vendor: strings.TrimSpace(vendor),
VendorID: vendorID,
@@ -2905,11 +3297,29 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
PartNumber: asString(doc["PartNumber"]),
Firmware: firmware,
PortCount: portCount,
LinkWidth: linkWidth,
LinkSpeed: linkSpeed,
MaxLinkWidth: maxLinkWidth,
MaxLinkSpeed: maxLinkSpeed,
Status: mapStatus(doc["Status"]),
Details: redfishPCIeDetails(doc, nil),
}
}
func isWeakRedfishNICSlotLabel(slot string) bool {
slot = strings.TrimSpace(slot)
if slot == "" {
return true
}
if isNumericString(slot) {
return true
}
if strings.EqualFold(slot, "nic") || strings.HasPrefix(strings.ToLower(slot), "nic") && !strings.Contains(strings.ToLower(slot), "slot") {
return true
}
return false
}
func networkAdapterPCIeDevicePaths(doc map[string]interface{}) []string {
var out []string
if controllers, ok := doc["Controllers"].([]interface{}); ok {
@@ -2967,7 +3377,7 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{
}
for _, fn := range functionDocs {
if strings.TrimSpace(nic.BDF) == "" {
nic.BDF = asString(fn["FunctionId"])
nic.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if nic.VendorID == 0 {
nic.VendorID = asHexOrInt(fn["VendorId"])
@@ -3132,9 +3542,13 @@ func redfishPCIeDetailsWithSupplementalDocs(doc map[string]interface{}, function
lookupDocs = append(lookupDocs, functionDocs...)
lookupDocs = append(lookupDocs, supplementalDocs...)
details := make(map[string]any)
addFloatDetail(details, "temperature_c", redfishFirstNumericAcrossDocs(lookupDocs,
temperatureC := redfishFirstNumericAcrossDocs(lookupDocs,
"TemperatureCelsius", "TemperatureC", "Temperature",
))
)
if temperatureC == 0 {
temperatureC = redfishSupplementalThermalMetricForPCIe(doc, supplementalDocs)
}
addFloatDetail(details, "temperature_c", temperatureC)
addFloatDetail(details, "power_w", redfishFirstNumericAcrossDocs(lookupDocs,
"PowerConsumedWatts", "PowerWatts", "PowerConsumptionWatts",
))
@@ -3189,6 +3603,89 @@ func redfishPCIeDetailsWithSupplementalDocs(doc map[string]interface{}, function
return details
}
func redfishSupplementalThermalMetricForPCIe(doc map[string]interface{}, supplementalDocs []map[string]interface{}) float64 {
if len(supplementalDocs) == 0 {
return 0
}
deviceNames := redfishPCIeSupplementalDeviceNames(doc)
if len(deviceNames) == 0 {
return 0
}
for _, supplemental := range supplementalDocs {
readings, ok := supplemental["TemperatureReadingsCelsius"].([]interface{})
if !ok || len(readings) == 0 {
continue
}
for _, readingAny := range readings {
reading, ok := readingAny.(map[string]interface{})
if !ok {
continue
}
deviceName := strings.TrimSpace(asString(reading["DeviceName"]))
if deviceName == "" || !matchesAnyFold(deviceName, deviceNames) {
continue
}
if value := asFloat(reading["Reading"]); value != 0 {
return value
}
}
}
return 0
}
func redfishPCIeSupplementalDeviceNames(doc map[string]interface{}) []string {
names := make([]string, 0, 3)
for _, raw := range []string{
asString(doc["Id"]),
asString(doc["Name"]),
asString(doc["Model"]),
} {
name := strings.TrimSpace(raw)
if name == "" {
continue
}
if !matchesAnyFold(name, names) {
names = append(names, name)
}
}
return names
}
func matchesAnyFold(value string, candidates []string) bool {
for _, candidate := range candidates {
if strings.EqualFold(strings.TrimSpace(value), strings.TrimSpace(candidate)) {
return true
}
}
return false
}
func looksLikeNVSwitchPCIeDoc(doc map[string]interface{}) bool {
joined := strings.ToLower(strings.TrimSpace(strings.Join([]string{
asString(doc["Id"]),
asString(doc["Name"]),
asString(doc["Model"]),
asString(doc["Manufacturer"]),
}, " ")))
return strings.Contains(joined, "nvswitch")
}
func chassisPathForPCIeDoc(docPath string) string {
docPath = normalizeRedfishPath(docPath)
if !strings.Contains(docPath, "/PCIeDevices/") {
return ""
}
idx := strings.Index(docPath, "/PCIeDevices/")
if idx <= 0 {
return ""
}
chassisPath := docPath[:idx]
if !strings.HasPrefix(chassisPath, "/redfish/v1/Chassis/") {
return ""
}
return chassisPath
}
func redfishFirstNumeric(doc map[string]interface{}, keys ...string) float64 {
for _, key := range keys {
if v, ok := redfishLookupValue(doc, key); ok {
@@ -3392,7 +3889,7 @@ func parseGPUWithSupplementalDocs(doc map[string]interface{}, functionDocs []map
Details: redfishPCIeDetailsWithSupplementalDocs(doc, functionDocs, supplementalDocs),
}
if bdf := asString(doc["BDF"]); bdf != "" {
if bdf := sanitizeRedfishBDF(asString(doc["BDF"])); bdf != "" {
gpu.BDF = bdf
}
if gpu.BDF == "" {
@@ -3407,7 +3904,7 @@ func parseGPUWithSupplementalDocs(doc map[string]interface{}, functionDocs []map
for _, fn := range functionDocs {
if gpu.BDF == "" {
gpu.BDF = asString(fn["FunctionId"])
gpu.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if gpu.VendorID == 0 {
gpu.VendorID = asHexOrInt(fn["VendorId"])
@@ -3448,7 +3945,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDocs []map[string]interface{}, supplementalDocs []map[string]interface{}) models.PCIeDevice {
dev := models.PCIeDevice{
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
BDF: asString(doc["BDF"]),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
DeviceClass: asString(doc["DeviceType"]),
Manufacturer: asString(doc["Manufacturer"]),
PartNumber: asString(doc["PartNumber"]),
@@ -3463,7 +3960,7 @@ func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDoc
for _, fn := range functionDocs {
if dev.BDF == "" {
dev.BDF = asString(fn["FunctionId"])
dev.BDF = sanitizeRedfishBDF(asString(fn["FunctionId"]))
}
if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"]))
@@ -3522,7 +4019,7 @@ func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplemen
dev := models.PCIeDevice{
Slot: slot,
BDF: asString(doc["FunctionId"]),
BDF: sanitizeRedfishBDF(asString(doc["BDF"])),
VendorID: asHexOrInt(doc["VendorId"]),
DeviceID: asHexOrInt(doc["DeviceId"]),
DeviceClass: firstNonEmpty(asString(doc["DeviceClass"]), asString(doc["ClassCode"]), "PCIe device"),
@@ -3534,6 +4031,9 @@ func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplemen
MaxLinkSpeed: firstNonEmpty(asString(doc["MaxLinkSpeedGTs"]), asString(doc["MaxLinkSpeed"])),
Details: redfishPCIeDetailsWithSupplementalDocs(doc, nil, supplementalDocs),
}
if dev.BDF == "" {
dev.BDF = firstNonEmpty(buildBDFfromOemPublic(doc), sanitizeRedfishBDF(asString(doc["FunctionId"])))
}
if isGenericPCIeClassLabel(dev.DeviceClass) {
if resolved := pciids.DeviceName(dev.VendorID, dev.DeviceID); resolved != "" {
dev.DeviceClass = resolved
@@ -3951,7 +4451,24 @@ func isVirtualStorageDrive(doc map[string]interface{}) bool {
}
mfr := strings.ToUpper(strings.TrimSpace(asString(doc["Manufacturer"])))
model := strings.ToUpper(strings.TrimSpace(asString(doc["Model"])))
if strings.Contains(mfr, "AMI") && strings.Contains(model, "VIRTUAL") {
name := strings.ToUpper(strings.TrimSpace(asString(doc["Name"])))
joined := strings.Join([]string{mfr, model, name}, " ")
if strings.Contains(mfr, "AMI") && strings.Contains(joined, "VIRTUAL") {
return true
}
for _, marker := range []string{
"VIRTUAL CDROM",
"VIRTUAL CD/DVD",
"VIRTUAL FLOPPY",
"VIRTUAL FDD",
"VIRTUAL USB",
"VIRTUAL MEDIA",
} {
if strings.Contains(joined, marker) {
return true
}
}
if strings.Contains(mfr, "AMERICAN MEGATRENDS") && (strings.Contains(joined, "CDROM") || strings.Contains(joined, "FLOPPY") || strings.Contains(joined, "FDD")) {
return true
}
return false