Improve Redfish raw replay recovery and GUI diagnostics
This commit is contained in:
@@ -23,8 +23,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type RedfishConnector struct {
|
type RedfishConnector struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
debug bool
|
debug bool
|
||||||
debugSnapshot bool
|
debugSnapshot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,15 +88,19 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
emit(Progress{Status: "running", Progress: 90, Message: "Redfish: сбор расширенного snapshot..."})
|
emit(Progress{Status: "running", Progress: 90, Message: "Redfish: сбор расширенного snapshot..."})
|
||||||
}
|
}
|
||||||
c.debugSnapshotf("snapshot crawl start host=%s port=%d", req.Host, req.Port)
|
c.debugSnapshotf("snapshot crawl start host=%s port=%d", req.Host, req.Port)
|
||||||
rawTree := c.collectRawRedfishTree(ctx, client, req, baseURL, redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths), emit)
|
rawTree, fetchErrors := c.collectRawRedfishTree(ctx, client, req, baseURL, redfishSnapshotPrioritySeeds(systemPaths, chassisPaths, managerPaths), emit)
|
||||||
c.debugSnapshotf("snapshot crawl done docs=%d", len(rawTree))
|
c.debugSnapshotf("snapshot crawl done docs=%d", len(rawTree))
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
||||||
}
|
}
|
||||||
// Unified tunnel: live collection and raw import go through the same analyzer over redfish_tree.
|
rawPayloads := map[string]any{
|
||||||
return ReplayRedfishFromRawPayloads(map[string]any{
|
|
||||||
"redfish_tree": rawTree,
|
"redfish_tree": rawTree,
|
||||||
}, nil)
|
}
|
||||||
|
if len(fetchErrors) > 0 {
|
||||||
|
rawPayloads["redfish_fetch_errors"] = fetchErrors
|
||||||
|
}
|
||||||
|
// Unified tunnel: live collection and raw import go through the same analyzer over redfish_tree.
|
||||||
|
return ReplayRedfishFromRawPayloads(rawPayloads, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RedfishConnector) httpClient(req Request) *http.Client {
|
func (c *RedfishConnector) httpClient(req Request) *http.Client {
|
||||||
@@ -444,7 +448,7 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
|
|||||||
for _, doc := range memberDocs {
|
for _, doc := range memberDocs {
|
||||||
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
|
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
|
||||||
dev := parsePCIeDevice(doc, functionDocs)
|
dev := parsePCIeDevice(doc, functionDocs)
|
||||||
key := firstNonEmpty(dev.SerialNumber, dev.BDF, dev.Slot+"|"+dev.DeviceClass)
|
key := firstNonEmpty(dev.BDF, dev.SerialNumber, dev.Slot+"|"+dev.DeviceClass)
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -506,12 +510,13 @@ func (c *RedfishConnector) discoverMemberPaths(ctx context.Context, client *http
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *http.Client, req Request, baseURL string, seedPaths []string, emit ProgressFn) map[string]interface{} {
|
func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *http.Client, req Request, baseURL string, seedPaths []string, emit ProgressFn) (map[string]interface{}, []map[string]interface{}) {
|
||||||
maxDocuments := redfishSnapshotMaxDocuments()
|
maxDocuments := redfishSnapshotMaxDocuments()
|
||||||
const workers = 6
|
const workers = 6
|
||||||
const heartbeatInterval = 5 * time.Second
|
const heartbeatInterval = 5 * time.Second
|
||||||
|
|
||||||
out := make(map[string]interface{}, maxDocuments)
|
out := make(map[string]interface{}, maxDocuments)
|
||||||
|
fetchErrors := make(map[string]string)
|
||||||
seen := make(map[string]struct{}, maxDocuments)
|
seen := make(map[string]struct{}, maxDocuments)
|
||||||
rootCounts := make(map[string]int)
|
rootCounts := make(map[string]int)
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
@@ -602,15 +607,20 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
enqueue(ref)
|
enqueue(ref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n := atomic.AddInt32(&processed, 1)
|
n := atomic.AddInt32(&processed, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
|
mu.Lock()
|
||||||
if emit != nil && shouldReportSnapshotFetchError(err) {
|
if _, ok := fetchErrors[current]; !ok {
|
||||||
emit(Progress{
|
fetchErrors[current] = err.Error()
|
||||||
Status: "running",
|
}
|
||||||
Progress: 92 + int(minInt32(n/200, 6)),
|
mu.Unlock()
|
||||||
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
|
c.debugSnapshotf("worker=%d fetch error path=%s err=%v", workerID, current, err)
|
||||||
})
|
if emit != nil && shouldReportSnapshotFetchError(err) {
|
||||||
|
emit(Progress{
|
||||||
|
Status: "running",
|
||||||
|
Progress: 92 + int(minInt32(n/200, 6)),
|
||||||
|
Message: fmt.Sprintf("Redfish snapshot: ошибка на %s", compactProgressPath(current)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if emit != nil && n%40 == 0 {
|
if emit != nil && n%40 == 0 {
|
||||||
@@ -677,7 +687,18 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
errorList := make([]map[string]interface{}, 0, len(fetchErrors))
|
||||||
|
for p, msg := range fetchErrors {
|
||||||
|
errorList = append(errorList, map[string]interface{}{
|
||||||
|
"path": p,
|
||||||
|
"error": msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(errorList, func(i, j int) bool {
|
||||||
|
return asString(errorList[i]["path"]) < asString(errorList[j]["path"])
|
||||||
|
})
|
||||||
|
|
||||||
|
return out, errorList
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} {
|
func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package collector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
)
|
)
|
||||||
@@ -64,12 +66,10 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
networkProtocolDoc, _ := r.getJSON(joinPath(primaryManager, "/NetworkProtocol"))
|
||||||
|
|
||||||
result := &models.AnalysisResult{
|
result := &models.AnalysisResult{
|
||||||
Events: make([]models.Event, 0),
|
Events: make([]models.Event, 0),
|
||||||
FRU: make([]models.FRUInfo, 0),
|
FRU: make([]models.FRUInfo, 0),
|
||||||
Sensors: make([]models.SensorReading, 0),
|
Sensors: make([]models.SensorReading, 0),
|
||||||
RawPayloads: map[string]any{
|
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||||
"redfish_tree": tree,
|
|
||||||
},
|
|
||||||
Hardware: &models.HardwareConfig{
|
Hardware: &models.HardwareConfig{
|
||||||
BoardInfo: parseBoardInfo(systemDoc),
|
BoardInfo: parseBoardInfo(systemDoc),
|
||||||
CPUs: parseCPUs(processors),
|
CPUs: parseCPUs(processors),
|
||||||
@@ -115,11 +115,11 @@ func (r redfishSnapshotReader) getJSON(requestPath string) (map[string]interface
|
|||||||
func (r redfishSnapshotReader) getCollectionMembers(collectionPath string) ([]map[string]interface{}, error) {
|
func (r redfishSnapshotReader) getCollectionMembers(collectionPath string) ([]map[string]interface{}, error) {
|
||||||
collection, err := r.getJSON(collectionPath)
|
collection, err := r.getJSON(collectionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return r.fallbackCollectionMembers(collectionPath, err)
|
||||||
}
|
}
|
||||||
refs, ok := collection["Members"].([]interface{})
|
refs, ok := collection["Members"].([]interface{})
|
||||||
if !ok || len(refs) == 0 {
|
if !ok || len(refs) == 0 {
|
||||||
return []map[string]interface{}{}, nil
|
return r.fallbackCollectionMembers(collectionPath, nil)
|
||||||
}
|
}
|
||||||
out := make([]map[string]interface{}, 0, len(refs))
|
out := make([]map[string]interface{}, 0, len(refs))
|
||||||
for _, refAny := range refs {
|
for _, refAny := range refs {
|
||||||
@@ -137,9 +137,61 @@ func (r redfishSnapshotReader) getCollectionMembers(collectionPath string) ([]ma
|
|||||||
}
|
}
|
||||||
out = append(out, doc)
|
out = append(out, doc)
|
||||||
}
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return r.fallbackCollectionMembers(collectionPath, nil)
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redfishSnapshotReader) fallbackCollectionMembers(collectionPath string, originalErr error) ([]map[string]interface{}, error) {
|
||||||
|
prefix := strings.TrimSuffix(normalizeRedfishPath(collectionPath), "/") + "/"
|
||||||
|
if prefix == "/" {
|
||||||
|
if originalErr != nil {
|
||||||
|
return nil, originalErr
|
||||||
|
}
|
||||||
|
return []map[string]interface{}{}, nil
|
||||||
|
}
|
||||||
|
paths := make([]string, 0)
|
||||||
|
for key := range r.tree {
|
||||||
|
p := normalizeRedfishPath(key)
|
||||||
|
if !strings.HasPrefix(p, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(p, prefix)
|
||||||
|
if rest == "" || strings.Contains(rest, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
paths = append(paths, p)
|
||||||
|
}
|
||||||
|
if len(paths) == 0 {
|
||||||
|
if originalErr != nil {
|
||||||
|
return nil, originalErr
|
||||||
|
}
|
||||||
|
return []map[string]interface{}{}, nil
|
||||||
|
}
|
||||||
|
sort.Strings(paths)
|
||||||
|
out := make([]map[string]interface{}, 0, len(paths))
|
||||||
|
for _, p := range paths {
|
||||||
|
doc, err := r.getJSON(p)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, doc)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneRawPayloads(src map[string]any) map[string]any {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dst := make(map[string]any, len(src))
|
||||||
|
for k, v := range src {
|
||||||
|
dst[k] = v
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
func (r redfishSnapshotReader) discoverMemberPaths(collectionPath, fallbackPath string) []string {
|
func (r redfishSnapshotReader) discoverMemberPaths(collectionPath, fallbackPath string) []string {
|
||||||
collection, err := r.getJSON(collectionPath)
|
collection, err := r.getJSON(collectionPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -482,7 +534,7 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
|||||||
for _, doc := range memberDocs {
|
for _, doc := range memberDocs {
|
||||||
functionDocs := r.getLinkedPCIeFunctions(doc)
|
functionDocs := r.getLinkedPCIeFunctions(doc)
|
||||||
dev := parsePCIeDevice(doc, functionDocs)
|
dev := parsePCIeDevice(doc, functionDocs)
|
||||||
key := firstNonEmpty(dev.SerialNumber, dev.BDF, dev.Slot+"|"+dev.DeviceClass)
|
key := firstNonEmpty(dev.BDF, dev.SerialNumber, dev.Slot+"|"+dev.DeviceClass)
|
||||||
if key == "" {
|
if key == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ func TestRedfishConnectorCollect(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
register("/redfish/v1/Systems/1/Memory/DIMM1", map[string]interface{}{
|
register("/redfish/v1/Systems/1/Memory/DIMM1", map[string]interface{}{
|
||||||
"Name": "DIMM A1",
|
"Name": "DIMM A1",
|
||||||
"CapacityMiB": 32768,
|
"CapacityMiB": 32768,
|
||||||
"MemoryDeviceType": "DDR5",
|
"MemoryDeviceType": "DDR5",
|
||||||
"OperatingSpeedMhz": 4800,
|
"OperatingSpeedMhz": 4800,
|
||||||
"Status": map[string]interface{}{
|
"Status": map[string]interface{}{
|
||||||
"Health": "OK",
|
"Health": "OK",
|
||||||
@@ -91,14 +91,14 @@ func TestRedfishConnectorCollect(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
register("/redfish/v1/Systems/1/PCIeFunctions/GPU1F0", map[string]interface{}{
|
register("/redfish/v1/Systems/1/PCIeFunctions/GPU1F0", map[string]interface{}{
|
||||||
"FunctionId": "0000:65:00.0",
|
"FunctionId": "0000:65:00.0",
|
||||||
"VendorId": "0x10DE",
|
"VendorId": "0x10DE",
|
||||||
"DeviceId": "0x2331",
|
"DeviceId": "0x2331",
|
||||||
"ClassCode": "0x030200",
|
"ClassCode": "0x030200",
|
||||||
"CurrentLinkWidth": 16,
|
"CurrentLinkWidth": 16,
|
||||||
"CurrentLinkSpeed": "16.0 GT/s",
|
"CurrentLinkSpeed": "16.0 GT/s",
|
||||||
"MaxLinkWidth": 16,
|
"MaxLinkWidth": 16,
|
||||||
"MaxLinkSpeed": "16.0 GT/s",
|
"MaxLinkSpeed": "16.0 GT/s",
|
||||||
})
|
})
|
||||||
register("/redfish/v1/Chassis/1/NetworkAdapters", map[string]interface{}{
|
register("/redfish/v1/Chassis/1/NetworkAdapters", map[string]interface{}{
|
||||||
"Members": []map[string]string{
|
"Members": []map[string]string{
|
||||||
@@ -239,6 +239,68 @@ func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReplayRedfishFromRawPayloads_FallbackCollectionMembersByPrefix(t *testing.T) {
|
||||||
|
raw := map[string]any{
|
||||||
|
"redfish_tree": map[string]interface{}{
|
||||||
|
"/redfish/v1": map[string]interface{}{
|
||||||
|
"Systems": map[string]interface{}{"@odata.id": "/redfish/v1/Systems"},
|
||||||
|
"Chassis": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis"},
|
||||||
|
"Managers": map[string]interface{}{"@odata.id": "/redfish/v1/Managers"},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Systems/1": map[string]interface{}{
|
||||||
|
"Manufacturer": "Supermicro",
|
||||||
|
"Model": "SYS-TEST",
|
||||||
|
"SerialNumber": "SYS123",
|
||||||
|
},
|
||||||
|
// Intentionally missing /redfish/v1/Systems/1/Processors collection.
|
||||||
|
"/redfish/v1/Systems/1/Processors/CPU1": map[string]interface{}{
|
||||||
|
"Id": "CPU1",
|
||||||
|
"Model": "Xeon Gold",
|
||||||
|
"TotalCores": 32,
|
||||||
|
"TotalThreads": 64,
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Chassis/1": map[string]interface{}{
|
||||||
|
"Id": "1",
|
||||||
|
},
|
||||||
|
"/redfish/v1/Managers": map[string]interface{}{
|
||||||
|
"Members": []interface{}{
|
||||||
|
map[string]interface{}{"@odata.id": "/redfish/v1/Managers/1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/redfish/v1/Managers/1": map[string]interface{}{
|
||||||
|
"Id": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"redfish_fetch_errors": []map[string]interface{}{
|
||||||
|
{"path": "/redfish/v1/Systems/1/Processors", "error": "status 500"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ReplayRedfishFromRawPayloads(raw, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("replay failed: %v", err)
|
||||||
|
}
|
||||||
|
if got.Hardware == nil {
|
||||||
|
t.Fatalf("expected hardware")
|
||||||
|
}
|
||||||
|
if len(got.Hardware.CPUs) != 1 {
|
||||||
|
t.Fatalf("expected one CPU via prefix fallback, got %d", len(got.Hardware.CPUs))
|
||||||
|
}
|
||||||
|
if _, ok := got.RawPayloads["redfish_fetch_errors"]; !ok {
|
||||||
|
t.Fatalf("expected raw payloads to preserve redfish_fetch_errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
func TestEnrichNICFromPCIeFunctions(t *testing.T) {
|
||||||
nic := parseNIC(map[string]interface{}{
|
nic := parseNIC(map[string]interface{}{
|
||||||
"Id": "1",
|
"Id": "1",
|
||||||
@@ -333,7 +395,7 @@ func TestReplayCollectStorage_ProbesSupermicroNVMeDiskBayWhenCollectionEmpty(t *
|
|||||||
},
|
},
|
||||||
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{
|
||||||
"Members@odata.count": 0,
|
"Members@odata.count": 0,
|
||||||
"Members": []interface{}{},
|
"Members": []interface{}{},
|
||||||
},
|
},
|
||||||
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.0": map[string]interface{}{
|
"/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.0": map[string]interface{}{
|
||||||
"Id": "Disk.Bay.0",
|
"Id": "Disk.Bay.0",
|
||||||
|
|||||||
@@ -319,6 +319,11 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
"target_host": result.TargetHost,
|
"target_host": result.TargetHost,
|
||||||
"collected_at": result.CollectedAt,
|
"collected_at": result.CollectedAt,
|
||||||
}
|
}
|
||||||
|
if result.RawPayloads != nil {
|
||||||
|
if fetchErrors, ok := result.RawPayloads["redfish_fetch_errors"]; ok {
|
||||||
|
response["redfish_fetch_errors"] = fetchErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if result.Hardware == nil {
|
if result.Hardware == nil {
|
||||||
response["hardware"] = map[string]interface{}{}
|
response["hardware"] = map[string]interface{}{}
|
||||||
|
|||||||
@@ -646,6 +646,8 @@ function renderConfig(data) {
|
|||||||
|
|
||||||
const config = data.hardware || data;
|
const config = data.hardware || data;
|
||||||
const spec = data.specification;
|
const spec = data.specification;
|
||||||
|
const redfishFetchErrors = Array.isArray(data.redfish_fetch_errors) ? data.redfish_fetch_errors : [];
|
||||||
|
const visibleRedfishFetchErrors = filterVisibleRedfishFetchErrors(redfishFetchErrors);
|
||||||
const devices = Array.isArray(config.devices) ? config.devices : [];
|
const devices = Array.isArray(config.devices) ? config.devices : [];
|
||||||
const volumes = Array.isArray(config.volumes) ? config.volumes : [];
|
const volumes = Array.isArray(config.volumes) ? config.volumes : [];
|
||||||
|
|
||||||
@@ -688,6 +690,32 @@ function renderConfig(data) {
|
|||||||
|
|
||||||
// Specification tab
|
// Specification tab
|
||||||
html += '<div class="config-tab-content active" id="config-spec">';
|
html += '<div class="config-tab-content active" id="config-spec">';
|
||||||
|
const partialInventory = detectPartialRedfishInventory({
|
||||||
|
cpus,
|
||||||
|
memory,
|
||||||
|
redfishFetchErrors
|
||||||
|
});
|
||||||
|
if (partialInventory) {
|
||||||
|
html += `<div class="spec-section">
|
||||||
|
<h3>Частичный инвентарь</h3>
|
||||||
|
<p class="no-data" style="margin-top: 0;">${escapeHtml(partialInventory)}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
if (visibleRedfishFetchErrors.length > 0) {
|
||||||
|
html += `<div class="spec-section">
|
||||||
|
<h3>Redfish fetch errors (${visibleRedfishFetchErrors.length})</h3>
|
||||||
|
<p class="no-data" style="margin-top: 0;">Сохранено в raw snapshot для последующего анализа в GUI.</p>
|
||||||
|
<table class="config-table"><thead><tr><th>Endpoint</th><th>Ошибка</th></tr></thead><tbody>`;
|
||||||
|
visibleRedfishFetchErrors.forEach(item => {
|
||||||
|
const path = item && typeof item === 'object' ? (item.path || '-') : '-';
|
||||||
|
const err = item && typeof item === 'object' ? (item.error || '-') : String(item || '-');
|
||||||
|
html += `<tr>
|
||||||
|
<td><code>${escapeHtml(String(path))}</code></td>
|
||||||
|
<td>${escapeHtml(String(err))}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
}
|
||||||
if (spec && spec.length > 0) {
|
if (spec && spec.length > 0) {
|
||||||
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
||||||
spec.forEach(item => {
|
spec.forEach(item => {
|
||||||
@@ -1319,6 +1347,30 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterVisibleRedfishFetchErrors(items) {
|
||||||
|
if (!Array.isArray(items)) return [];
|
||||||
|
return items.filter(item => {
|
||||||
|
const message = String(item && typeof item === 'object' ? (item.error || '') : item || '').toLowerCase();
|
||||||
|
return !(
|
||||||
|
message.startsWith('status 404 ') ||
|
||||||
|
message.startsWith('status 405 ') ||
|
||||||
|
message.startsWith('status 410 ') ||
|
||||||
|
message.startsWith('status 501 ')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPartialRedfishInventory({ cpus, memory, redfishFetchErrors }) {
|
||||||
|
const errors = Array.isArray(redfishFetchErrors) ? redfishFetchErrors : [];
|
||||||
|
const paths = errors.map(item => String(item && typeof item === 'object' ? (item.path || '') : '')).filter(Boolean);
|
||||||
|
const cpuMissing = (!Array.isArray(cpus) || cpus.length === 0) && paths.some(p => /\/Systems\/[^/]+\/Processors(\/)?$/i.test(p));
|
||||||
|
const memMissing = (!Array.isArray(memory) || memory.length === 0) && paths.some(p => /\/Systems\/[^/]+\/Memory(\/)?$/i.test(p));
|
||||||
|
if (!cpuMissing && !memMissing) return '';
|
||||||
|
if (cpuMissing && memMissing) return 'Не удалось восстановить CPU и Memory: Redfish endpoint\'ы /Processors и /Memory были недоступны во время сбора.';
|
||||||
|
if (cpuMissing) return 'CPU-инвентарь неполный: Redfish endpoint /Processors был недоступен во время сбора.';
|
||||||
|
return 'Memory-инвентарь неполный: Redfish endpoint /Memory был недоступен во время сбора.';
|
||||||
|
}
|
||||||
|
|
||||||
function calculateCPUToPCIeBalance(inventoryRows, cpus) {
|
function calculateCPUToPCIeBalance(inventoryRows, cpus) {
|
||||||
const laneByCPU = new Map();
|
const laneByCPU = new Map();
|
||||||
const cpuIndexes = new Set();
|
const cpuIndexes = new Set();
|
||||||
|
|||||||
Reference in New Issue
Block a user