export: align reanimator contract v2.7
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -93,12 +93,12 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
||||
collectedAt, sourceTimezone := inferRedfishCollectionTime(managerDoc, rawPayloads)
|
||||
|
||||
result := &models.AnalysisResult{
|
||||
CollectedAt: collectedAt,
|
||||
CollectedAt: collectedAt,
|
||||
SourceTimezone: sourceTimezone,
|
||||
Events: append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
Events: append(append(append(make([]models.Event, 0, len(discreteEvents)+len(healthEvents)+len(driveFetchWarningEvents)+1), healthEvents...), discreteEvents...), driveFetchWarningEvents...),
|
||||
FRU: assemblyFRU,
|
||||
Sensors: dedupeSensorReadings(append(append(thresholdSensors, thermalSensors...), powerSensors...)),
|
||||
RawPayloads: cloneRawPayloads(rawPayloads),
|
||||
Hardware: &models.HardwareConfig{
|
||||
BoardInfo: boardInfo,
|
||||
CPUs: processors,
|
||||
@@ -273,11 +273,7 @@ func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo
|
||||
if strings.TrimSpace(version) == "" {
|
||||
continue
|
||||
}
|
||||
name := firstNonEmpty(
|
||||
asString(doc["DeviceName"]),
|
||||
asString(doc["Name"]),
|
||||
asString(doc["Id"]),
|
||||
)
|
||||
name := firmwareInventoryDeviceName(doc)
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
@@ -291,6 +287,25 @@ func (r redfishSnapshotReader) collectFirmwareInventory() []models.FirmwareInfo
|
||||
return out
|
||||
}
|
||||
|
||||
func firmwareInventoryDeviceName(doc map[string]interface{}) string {
|
||||
name := strings.TrimSpace(asString(doc["DeviceName"]))
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(asString(doc["Id"]))
|
||||
rawName := strings.TrimSpace(asString(doc["Name"]))
|
||||
if strings.EqualFold(rawName, "Software Inventory") || strings.EqualFold(rawName, "Firmware Inventory") {
|
||||
if id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
if rawName != "" {
|
||||
return rawName
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func dedupeFirmwareInfo(items []models.FirmwareInfo) []models.FirmwareInfo {
|
||||
seen := make(map[string]struct{}, len(items))
|
||||
out := make([]models.FirmwareInfo, 0, len(items))
|
||||
@@ -1094,6 +1109,9 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
continue
|
||||
}
|
||||
if looksLikeDrive(member) {
|
||||
if isVirtualStorageDrive(member) {
|
||||
continue
|
||||
}
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(member, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(member, supplementalDocs...))
|
||||
}
|
||||
@@ -1102,13 +1120,16 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
driveDocs, err := r.getCollectionMembers(joinPath(enclosurePath, "/Drives"))
|
||||
if err == nil {
|
||||
for _, driveDoc := range driveDocs {
|
||||
if looksLikeDrive(driveDoc) {
|
||||
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
}
|
||||
if len(driveDocs) == 0 {
|
||||
for _, driveDoc := range r.probeDirectDiskBayChildren(joinPath(enclosurePath, "/Drives")) {
|
||||
if isVirtualStorageDrive(driveDoc) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
}
|
||||
}
|
||||
@@ -1120,7 +1141,7 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
"/Storage/IntelVROC/Drives",
|
||||
"/Storage/IntelVROC/Controllers/1/Drives",
|
||||
}) {
|
||||
if looksLikeDrive(driveDoc) {
|
||||
if looksLikeDrive(driveDoc) && !isVirtualStorageDrive(driveDoc) {
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(driveDoc, "DriveMetrics", "EnvironmentMetrics", "Metrics")
|
||||
out = append(out, parseDriveWithSupplementalDocs(driveDoc, supplementalDocs...))
|
||||
}
|
||||
@@ -1134,7 +1155,7 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
}
|
||||
for _, devAny := range devices {
|
||||
devDoc, ok := devAny.(map[string]interface{})
|
||||
if !ok || !looksLikeDrive(devDoc) {
|
||||
if !ok || !looksLikeDrive(devDoc) || isVirtualStorageDrive(devDoc) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(devDoc))
|
||||
@@ -1148,7 +1169,7 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
continue
|
||||
}
|
||||
for _, driveDoc := range driveDocs {
|
||||
if !looksLikeDrive(driveDoc) {
|
||||
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
@@ -1159,7 +1180,7 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
|
||||
continue
|
||||
}
|
||||
for _, driveDoc := range r.probeSupermicroNVMeDiskBays(chassisPath) {
|
||||
if !looksLikeDrive(driveDoc) {
|
||||
if !looksLikeDrive(driveDoc) || isVirtualStorageDrive(driveDoc) {
|
||||
continue
|
||||
}
|
||||
out = append(out, parseDrive(driveDoc))
|
||||
@@ -1353,6 +1374,7 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
continue
|
||||
}
|
||||
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
|
||||
supplementalDocs = append(supplementalDocs, r.getChassisScopedPCIeSupplementalDocs(doc)...)
|
||||
for _, fn := range functionDocs {
|
||||
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
|
||||
}
|
||||
@@ -1377,6 +1399,29 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
||||
return dedupePCIeDevices(out)
|
||||
}
|
||||
|
||||
func (r redfishSnapshotReader) getChassisScopedPCIeSupplementalDocs(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)
|
||||
for _, path := range []string{
|
||||
joinPath(chassisPath, "/EnvironmentMetrics"),
|
||||
joinPath(chassisPath, "/ThermalSubsystem/ThermalMetrics"),
|
||||
} {
|
||||
supplementalDoc, err := r.getJSON(path)
|
||||
if err != nil || len(supplementalDoc) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, supplementalDoc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringsTrimTrailingSlash(s string) string {
|
||||
for len(s) > 1 && s[len(s)-1] == '/' {
|
||||
s = s[:len(s)-1]
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -206,6 +207,177 @@ func TestRedfishConnectorCollect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedfishConnectorProbe(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
register := func(path string, payload interface{}) {
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
})
|
||||
}
|
||||
|
||||
register("/redfish/v1", map[string]interface{}{"Name": "ServiceRoot"})
|
||||
register("/redfish/v1/Systems/1", map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerState": "Off",
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On", "ForceOff"},
|
||||
},
|
||||
},
|
||||
})
|
||||
ts := httptest.NewTLSServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
connector := NewRedfishConnector()
|
||||
port := 443
|
||||
host := ""
|
||||
if u, err := url.Parse(ts.URL); err == nil {
|
||||
host = u.Hostname()
|
||||
if p := u.Port(); p != "" {
|
||||
fmt.Sscanf(p, "%d", &port)
|
||||
}
|
||||
}
|
||||
got, err := connector.Probe(context.Background(), Request{
|
||||
Host: host,
|
||||
Protocol: "redfish",
|
||||
Port: port,
|
||||
Username: "admin",
|
||||
AuthType: "password",
|
||||
Password: "secret",
|
||||
TLSMode: "insecure",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("probe failed: %v", err)
|
||||
}
|
||||
if got == nil || !got.Reachable {
|
||||
t.Fatalf("expected reachable probe result, got %+v", got)
|
||||
}
|
||||
if got.HostPoweredOn {
|
||||
t.Fatalf("expected powered off host")
|
||||
}
|
||||
if got.HostPowerState != "Off" {
|
||||
t.Fatalf("expected power state Off, got %q", got.HostPowerState)
|
||||
}
|
||||
if !got.PowerControlAvailable {
|
||||
t.Fatalf("expected power control available")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_WaitsForStablePowerOn(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
|
||||
powerState := "Off"
|
||||
resetCalls := 0
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/redfish/v1/Systems/1", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerState": powerState,
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
resetCalls++
|
||||
powerState = "On"
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ts := httptest.NewTLSServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse server url: %v", err)
|
||||
}
|
||||
port := 443
|
||||
if u.Port() != "" {
|
||||
fmt.Sscanf(u.Port(), "%d", &port)
|
||||
}
|
||||
|
||||
c := NewRedfishConnector()
|
||||
hostOn, changed := c.ensureHostPowerForCollection(context.Background(), c.httpClientWithTimeout(Request{TLSMode: "insecure"}, 5*time.Second), Request{
|
||||
Host: u.Hostname(),
|
||||
Protocol: "redfish",
|
||||
Port: port,
|
||||
Username: "admin",
|
||||
AuthType: "password",
|
||||
Password: "secret",
|
||||
TLSMode: "insecure",
|
||||
PowerOnIfHostOff: true,
|
||||
}, ts.URL, "/redfish/v1/Systems/1", nil)
|
||||
if !hostOn || !changed {
|
||||
t.Fatalf("expected stable power-on result, got hostOn=%v changed=%v", hostOn, changed)
|
||||
}
|
||||
if resetCalls != 1 {
|
||||
t.Fatalf("expected one reset call, got %d", resetCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureHostPowerForCollection_FailsIfHostDoesNotStayOnAfterStabilization(t *testing.T) {
|
||||
t.Setenv("LOGPILE_REDFISH_POWERON_STABILIZATION", "1ms")
|
||||
|
||||
powerState := "Off"
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/redfish/v1/Systems/1", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
current := powerState
|
||||
if powerState == "On" {
|
||||
powerState = "Off"
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"PowerState": current,
|
||||
"Actions": map[string]interface{}{
|
||||
"#ComputerSystem.Reset": map[string]interface{}{
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": []interface{}{"On"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
powerState = "On"
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
ts := httptest.NewTLSServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
u, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse server url: %v", err)
|
||||
}
|
||||
port := 443
|
||||
if u.Port() != "" {
|
||||
fmt.Sscanf(u.Port(), "%d", &port)
|
||||
}
|
||||
|
||||
c := NewRedfishConnector()
|
||||
hostOn, changed := c.ensureHostPowerForCollection(context.Background(), c.httpClientWithTimeout(Request{TLSMode: "insecure"}, 5*time.Second), Request{
|
||||
Host: u.Hostname(),
|
||||
Protocol: "redfish",
|
||||
Port: port,
|
||||
Username: "admin",
|
||||
AuthType: "password",
|
||||
Password: "secret",
|
||||
TLSMode: "insecure",
|
||||
PowerOnIfHostOff: true,
|
||||
}, ts.URL, "/redfish/v1/Systems/1", nil)
|
||||
if hostOn || changed {
|
||||
t.Fatalf("expected unstable power-on result to fail, got hostOn=%v changed=%v", hostOn, changed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceSlot_FromNestedRedfishSlotLocation(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "NIC1",
|
||||
@@ -629,6 +801,42 @@ func TestParseNIC_PortCountFromControllerCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNIC_PrefersControllerSlotLabelAndPCIeInterface(t *testing.T) {
|
||||
nic := parseNIC(map[string]interface{}{
|
||||
"Id": "1",
|
||||
"Model": "MCX75310AAS-NEAT",
|
||||
"Manufacturer": "Supermicro",
|
||||
"Controllers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"Location": map[string]interface{}{
|
||||
"PartLocation": map[string]interface{}{
|
||||
"ServiceLabel": "PCIe Slot 1 (1)",
|
||||
},
|
||||
},
|
||||
"PCIeInterface": map[string]interface{}{
|
||||
"LanesInUse": 16,
|
||||
"MaxLanes": 16,
|
||||
"PCIeType": "Gen5",
|
||||
"MaxPCIeType": "Gen5",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if nic.Slot != "PCIe Slot 1 (1)" {
|
||||
t.Fatalf("expected slot from controller location, got %q", nic.Slot)
|
||||
}
|
||||
if nic.Location != "PCIe Slot 1 (1)" {
|
||||
t.Fatalf("expected location from controller location, got %q", nic.Location)
|
||||
}
|
||||
if nic.LinkWidth != 16 || nic.MaxLinkWidth != 16 {
|
||||
t.Fatalf("expected link widths from controller PCIeInterface, got current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth)
|
||||
}
|
||||
if nic.LinkSpeed != "Gen5" || nic.MaxLinkSpeed != "Gen5" {
|
||||
t.Fatalf("expected link speeds from controller PCIeInterface, got current=%q max=%q", nic.LinkSpeed, nic.MaxLinkSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNIC_DropsUnrealisticPortCount(t *testing.T) {
|
||||
nic := parseNIC(map[string]interface{}{
|
||||
"Id": "1",
|
||||
@@ -670,6 +878,48 @@ func TestParsePCIeDevice_PrefersFunctionClassOverDeviceType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeComponents_DoNotTreatNumericFunctionIDAsBDF(t *testing.T) {
|
||||
pcieFn := parsePCIeFunction(map[string]interface{}{
|
||||
"Id": "1",
|
||||
"FunctionId": "1",
|
||||
"DeviceClass": "NetworkController",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
}, 1)
|
||||
if pcieFn.BDF != "" {
|
||||
t.Fatalf("expected empty BDF for numeric FunctionId, got %q", pcieFn.BDF)
|
||||
}
|
||||
|
||||
gpu := parseGPU(map[string]interface{}{
|
||||
"Id": "GPU1",
|
||||
"Name": "GPU1",
|
||||
"PCIeFunctions": map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/GPU1/PCIeFunctions",
|
||||
},
|
||||
}, []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "1",
|
||||
"VendorId": "0x10de",
|
||||
"DeviceId": "0x2331",
|
||||
},
|
||||
}, 1)
|
||||
if gpu.BDF != "" {
|
||||
t.Fatalf("expected GPU BDF to stay empty when only numeric FunctionId exists, got %q", gpu.BDF)
|
||||
}
|
||||
|
||||
nic := parseNIC(map[string]interface{}{"Id": "1"})
|
||||
enrichNICFromPCIe(&nic, map[string]interface{}{}, []map[string]interface{}{
|
||||
{
|
||||
"FunctionId": "1",
|
||||
"VendorId": "0x15b3",
|
||||
"DeviceId": "0x1021",
|
||||
},
|
||||
}, nil)
|
||||
if nic.BDF != "" {
|
||||
t.Fatalf("expected NIC BDF to stay empty when only numeric FunctionId exists, got %q", nic.BDF)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseComponents_UseNestedSerialNumberFallback(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Name": "dev0",
|
||||
@@ -727,9 +977,9 @@ func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||
"TemperatureCelsius": 63,
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"MicrocodeVersion": "0x2b000643",
|
||||
"UncorrectableErrors": 1,
|
||||
"ThermalThrottled": true,
|
||||
"ThermalThrottled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -749,18 +999,18 @@ func TestParseCPUAndMemory_CollectOemDetails(t *testing.T) {
|
||||
|
||||
dimms := parseMemory([]map[string]interface{}{
|
||||
{
|
||||
"Id": "DIMM0",
|
||||
"Id": "DIMM0",
|
||||
"DeviceLocator": "CPU0_C0D0",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"CapacityMiB": 32768,
|
||||
"SerialNumber": "DIMM-001",
|
||||
"Oem": map[string]interface{}{
|
||||
"VendorX": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 12,
|
||||
"UncorrectableECCErrorCount": 2,
|
||||
"TemperatureC": 41.5,
|
||||
"CorrectableECCErrorCount": 12,
|
||||
"UncorrectableECCErrorCount": 2,
|
||||
"TemperatureC": 41.5,
|
||||
"SpareBlocksRemainingPercent": 88,
|
||||
"PerformanceDegraded": true,
|
||||
"DataLossDetected": false,
|
||||
"PerformanceDegraded": true,
|
||||
"DataLossDetected": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -831,9 +1081,9 @@ func TestReplayRedfishFromRawPayloads_UsesProcessorAndMemoryMetrics(t *testing.T
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Memory/DIMM0/MemoryMetrics": map[string]interface{}{
|
||||
"CorrectableECCErrorCount": 14,
|
||||
"TemperatureCelsius": 42,
|
||||
"PerformanceDegraded": true,
|
||||
"CorrectableECCErrorCount": 14,
|
||||
"TemperatureCelsius": 42,
|
||||
"PerformanceDegraded": true,
|
||||
"SpareBlocksRemainingPercent": 91,
|
||||
},
|
||||
},
|
||||
@@ -897,10 +1147,10 @@ func TestReplayRedfishFromRawPayloads_UsesDriveMetrics(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"/redfish/v1/Systems/1/Storage/RAID1/Drives/Drive0/DriveMetrics": map[string]interface{}{
|
||||
"PowerOnHours": 1001,
|
||||
"MediaErrors": 3,
|
||||
"PowerOnHours": 1001,
|
||||
"MediaErrors": 3,
|
||||
"AvailableSparePercent": 92,
|
||||
"TemperatureCelsius": 37,
|
||||
"TemperatureCelsius": 37,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -978,9 +1228,9 @@ func TestParseDriveAndPSU_CollectComponentMetricsIntoDetails(t *testing.T) {
|
||||
"SerialNumber": "SSD-002",
|
||||
"Oem": map[string]interface{}{
|
||||
"Public": map[string]interface{}{
|
||||
"temperature": 19,
|
||||
"temperature": 19,
|
||||
"PercentAvailableSpare": 93,
|
||||
"PercentageUsed": 7,
|
||||
"PercentageUsed": 7,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1076,9 +1326,9 @@ func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
||||
"SerialNumber": "SSD-001",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"PowerOnHours": 5001,
|
||||
"MediaErrors": 2,
|
||||
"TemperatureC": 39.5,
|
||||
"PowerOnHours": 5001,
|
||||
"MediaErrors": 2,
|
||||
"TemperatureC": 39.5,
|
||||
"LifeUsedPercent": 9,
|
||||
},
|
||||
)
|
||||
@@ -1093,7 +1343,7 @@ func TestParseComponentDetails_UseLinkedSupplementalMetrics(t *testing.T) {
|
||||
},
|
||||
1,
|
||||
map[string]interface{}{
|
||||
"Temperature": 44,
|
||||
"Temperature": 44,
|
||||
"LifeRemainingPercent": 97,
|
||||
},
|
||||
)
|
||||
@@ -2206,6 +2456,48 @@ func TestLooksLikeGPU_NVSwitchExcluded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirmwareInventoryDeviceName_PrefersIDForGenericSoftwareInventory(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "HGX_FW_NVSwitch_0",
|
||||
"Name": "Software Inventory",
|
||||
"Version": "96.10.73.00.01",
|
||||
}
|
||||
|
||||
got := firmwareInventoryDeviceName(doc)
|
||||
if got != "HGX_FW_NVSwitch_0" {
|
||||
t.Fatalf("expected firmware inventory id to be used, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePCIeDeviceWithSupplementalDocs_NVSwitchThermalMetrics(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"@odata.id": "/redfish/v1/Chassis/HGX_NVSwitch_0/PCIeDevices/NVSwitch_0",
|
||||
"Id": "NVSwitch_0",
|
||||
"Model": "NVSwitch",
|
||||
"Manufacturer": "NVIDIA",
|
||||
"DeviceType": "SingleFunction",
|
||||
}
|
||||
|
||||
supplementalDocs := []map[string]interface{}{
|
||||
{
|
||||
"TemperatureReadingsCelsius": []interface{}{
|
||||
map[string]interface{}{
|
||||
"DeviceName": "NVSwitch_0",
|
||||
"Reading": "31.593750",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := parsePCIeDeviceWithSupplementalDocs(doc, nil, supplementalDocs)
|
||||
if got.Details == nil {
|
||||
t.Fatalf("expected NVSwitch details to be populated")
|
||||
}
|
||||
if temp := got.Details["temperature_c"]; temp != 31.59375 {
|
||||
t.Fatalf("expected NVSwitch thermal metric, got %#v", got.Details)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(t *testing.T) {
|
||||
if !shouldCrawlPath("/redfish/v1/Systems/1/Memory/CPU0_C0D0") {
|
||||
t.Fatalf("expected direct DIMM resource to be crawlable")
|
||||
@@ -2222,6 +2514,12 @@ func TestShouldCrawlPath_MemoryAndProcessorMetricsAreAllowed(t *testing.T) {
|
||||
if shouldCrawlPath("/redfish/v1/Chassis/1/PCIeDevices/0/PCIeFunctions/1") {
|
||||
t.Fatalf("expected noisy chassis pciefunctions branch to be skipped")
|
||||
}
|
||||
if !shouldCrawlPath("/redfish/v1/Fabrics/HGX_NVLinkFabric_0/Switches/NVSwitch_0") {
|
||||
t.Fatalf("expected NVSwitch fabric resource to be crawlable")
|
||||
}
|
||||
if !shouldCrawlPath("/redfish/v1/Fabrics/HGX_NVLinkFabric_0/Switches/NVSwitch_0/Ports/NVLink_0/Metrics") {
|
||||
t.Fatalf("expected NVLink port metrics to be crawlable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRedfishMemoryMemberPath(t *testing.T) {
|
||||
@@ -2531,3 +2829,19 @@ func TestNVMePostProbeSkipsNonStorageChassis(t *testing.T) {
|
||||
t.Fatalf("expected StorageBackplane to be selected, got %q", selected[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVirtualStorageDrive_AMIVirtualMedia(t *testing.T) {
|
||||
doc := map[string]interface{}{
|
||||
"Id": "USB_Device1_Port4",
|
||||
"Name": "Virtual Cdrom Device",
|
||||
"Model": "Virtual Cdrom Device",
|
||||
"Manufacturer": "American Megatrends Inc.",
|
||||
"Protocol": "USB",
|
||||
"CapacityBytes": 0,
|
||||
"SerialNumber": "AAAABBBBCCCC1",
|
||||
}
|
||||
|
||||
if !isVirtualStorageDrive(doc) {
|
||||
t.Fatalf("expected AMI virtual media to be filtered")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Host string
|
||||
Protocol string
|
||||
Port int
|
||||
Username string
|
||||
AuthType string
|
||||
Password string
|
||||
Token string
|
||||
TLSMode string
|
||||
Host string
|
||||
Protocol string
|
||||
Port int
|
||||
Username string
|
||||
AuthType string
|
||||
Password string
|
||||
Token string
|
||||
TLSMode string
|
||||
PowerOnIfHostOff bool
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
@@ -25,7 +26,20 @@ type Progress struct {
|
||||
|
||||
type ProgressFn func(Progress)
|
||||
|
||||
type ProbeResult struct {
|
||||
Reachable bool
|
||||
Protocol string
|
||||
HostPowerState string
|
||||
HostPoweredOn bool
|
||||
PowerControlAvailable bool
|
||||
SystemPath string
|
||||
}
|
||||
|
||||
type Connector interface {
|
||||
Protocol() string
|
||||
Collect(ctx context.Context, req Request, emit ProgressFn) (*models.AnalysisResult, error)
|
||||
}
|
||||
|
||||
type Prober interface {
|
||||
Probe(ctx context.Context, req Request) (*ProbeResult, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user