diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6eebc..e1e58fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,18 @@ log is based on the [Keep a CHANGELOG](http://keepachangelog.com/) project. - Add ability to reference different vault paths for credential retrieval [#25](https://github.com/Comcast/fishymetrics/issues/25) - Added HPE DL380 Gen10 support [#17](https://github.com/Comcast/fishymetrics/issues/17) - Enhanced drive metrics collection for DL380 model servers to include NVME, Storage Disk Drives, and Logical Drives [#17](https://github.com/Comcast/fishymetrics/issues/17) -- add ability to send logs directly to elasticsearch endpoints [#10](https://github.com/Comcast/fishymetrics/issues/10) +- Add ability to send logs directly to elasticsearch endpoints [#10](https://github.com/Comcast/fishymetrics/issues/10) +- Add HPE Proliant DL560 Gen9 support [#23](https://github.com/Comcast/fishymetrics/issues/23) +- Add HPE Proliant XL420 Support [#33](https://github.com/Comcast/fishymetrics/issues/33) ## Fixed - Cisco UCS C220 - add additional edge cases when collecting memory metrics [#2](https://github.com/Comcast/fishymetrics/issues/2) ## Updated - Enhanced drive metrics collection for HPE DL360 model servers to include NVME, Storage Disk Drives, and Logical Drives. [#31](https://github.com/Comcast/fishymetrics/issues/31) -- removed references to internal URLs/FQDNs to opensource the project +- Removed references to internal URLs/FQDNs to opensource the project +- Cisco S3260M5 module to support FW Ver 4.2(xx) [#18](https://github.com/Comcast/fishymetrics/issues/18) +- HP DL360 module to support responses from iLO4 [#34](https://github.com/Comcast/fishymetrics/issues/34) ## [0.7.1] diff --git a/README.md b/README.md index 52b0b63..b6494fc 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Current device models supported - HP DL360 - HP DL560 - HP DL20 +- HP XL420 - Cisco UCS C220 M5 - Cisco UCS S3260 M4 - Cisco UCS S3260 M5 @@ -142,7 +143,7 @@ comcast/fishymetrics:latest ## Prometheus Configuration The fishymetrics exporter needs to be passed the address as a parameter, this can be -done with relabelling. available module options `["moonshot", "dl360", "dl20", "dl380", "dl560", "c220", "s3260m4", "s3260m5"]` +done with relabelling. available module options `["moonshot", "dl360", "dl20", "dl380", "dl560", "xl420", "c220", "s3260m4", "s3260m5"]` Example config: ```YAML diff --git a/cmd/fishymetrics/main.go b/cmd/fishymetrics/main.go index c98273b..0c1fe0a 100644 --- a/cmd/fishymetrics/main.go +++ b/cmd/fishymetrics/main.go @@ -41,6 +41,7 @@ import ( "github.com/comcast/fishymetrics/hpe/dl380" "github.com/comcast/fishymetrics/hpe/dl560" "github.com/comcast/fishymetrics/hpe/moonshot" + "github.com/comcast/fishymetrics/hpe/xl420" "github.com/comcast/fishymetrics/logger" "github.com/comcast/fishymetrics/middleware/muxprom" fishy_vault "github.com/comcast/fishymetrics/vault" @@ -157,6 +158,8 @@ func handler(ctx context.Context, w http.ResponseWriter, r *http.Request) { exporter = dl560.NewExporter(r.Context(), target, uri, credProf) case "dl20": exporter = dl20.NewExporter(r.Context(), target, uri, credProf) + case "xl420": + exporter = xl420.NewExporter(r.Context(), target, uri, credProf) case "c220": exporter, err = c220.NewExporter(r.Context(), target, uri, credProf) case "s3260m4": @@ -165,7 +168,7 @@ func handler(ctx context.Context, w http.ResponseWriter, r *http.Request) { exporter, err = s3260m5.NewExporter(r.Context(), target, uri, credProf) default: log.Error("'module' parameter does not match available options", zap.String("module", moduleName), zap.String("target", target), zap.Any("trace_id", r.Context().Value("traceID"))) - http.Error(w, "'module' parameter does not match available options: [moonshot, dl360, dl380, dl560, dl20, c220, s3260m4, s3260m5]", http.StatusBadRequest) + http.Error(w, "'module' parameter does not match available options: [moonshot, dl360, dl380, dl560, dl20, xl420, c220, s3260m4, s3260m5]", http.StatusBadRequest) return } diff --git a/cmd/fishymetrics/templates.go b/cmd/fishymetrics/templates.go index 9d82a29..458cd3a 100644 --- a/cmd/fishymetrics/templates.go +++ b/cmd/fishymetrics/templates.go @@ -58,8 +58,9 @@ const indexTmpl string = ` - + + diff --git a/hpe/dl360/drive.go b/hpe/dl360/drive.go index bbb3207..abbc7cc 100644 --- a/hpe/dl360/drive.go +++ b/hpe/dl360/drive.go @@ -73,17 +73,28 @@ type GenericDrive struct { Members []struct { URL string `json:"@odata.id"` } `json:"Members,omitempty"` - Links struct { + Links *struct { Drives []struct { URL string `json:"@odata.id"` } `json:"Drives,omitempty"` - LogicalDrives struct { + LogicalDrives *struct { URL string `json:"@odata.id"` } `json:"LogicalDrives,omitempty"` - PhysicalDrives struct { + PhysicalDrives *struct { URL string `json:"@odata.id"` } `json:"PhysicalDrives,omitempty"` } `json:"Links,omitempty"` + Link *struct { + Drives []struct { + URL string `json:"href"` + } `json:"Drives,omitempty"` + LogicalDrives *struct { + URL string `json:"href"` + } `json:"LogicalDrives,omitempty"` + PhysicalDrives *struct { + URL string `json:"href"` + } `json:"PhysicalDrives,omitempty"` + } `json:"links,omitempty"` MembersCount int `json:"Members@odata.count,omitempty"` } diff --git a/hpe/dl360/exporter.go b/hpe/dl360/exporter.go index ffa3baf..8edddda 100644 --- a/hpe/dl360/exporter.go +++ b/hpe/dl360/exporter.go @@ -154,34 +154,70 @@ func NewExporter(ctx context.Context, target, uri, profile string) *Exporter { continue } - // If LogicalDrives is present, parse logical drive endpoint until all urls are found - if newOutput.Links.LogicalDrives.URL != "" { - logicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Links.LogicalDrives.URL, target, retryClient) - if err != nil { - log.Error("api call "+fqdn.String()+newOutput.Links.LogicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) - continue + if newOutput.Links != nil { + // If LogicalDrives is present, parse logical drive endpoint until all urls are found + if newOutput.Links.LogicalDrives != nil && newOutput.Links.LogicalDrives.URL != "" { + logicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Links.LogicalDrives.URL, target, retryClient) + if err != nil { + log.Error("api call "+fqdn.String()+newOutput.Links.LogicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + continue + } + + if logicalDriveOutput.MembersCount > 0 { + // loop through each Member in the "LogicalDrive" field + for _, member := range logicalDriveOutput.Members { + // append each URL in the Members array to the logicalDriveURLs array. + logicalDriveURLs = append(logicalDriveURLs, member.URL) + } + } } - if logicalDriveOutput.MembersCount > 0 { - // loop through each Member in the "LogicalDrive" field - for _, member := range logicalDriveOutput.Members { - // append each URL in the Members array to the logicalDriveURLs array. - logicalDriveURLs = append(logicalDriveURLs, member.URL) + // If PhysicalDrives is present, parse physical drive endpoint until all urls are found + if newOutput.Links.PhysicalDrives != nil && newOutput.Links.PhysicalDrives.URL != "" { + physicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Links.PhysicalDrives.URL, target, retryClient) + if err != nil { + log.Error("api call "+fqdn.String()+newOutput.Links.PhysicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + continue + } + + if physicalDriveOutput.MembersCount > 0 { + for _, member := range physicalDriveOutput.Members { + physicalDriveURLs = append(physicalDriveURLs, member.URL) + } } } } - // If PhysicalDrives is present, parse physical drive endpoint until all urls are found - if newOutput.Links.PhysicalDrives.URL != "" { - physicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Links.PhysicalDrives.URL, target, retryClient) + if newOutput.Link != nil { + // If LogicalDrives is present, parse logical drive endpoint until all urls are found + if newOutput.Link.LogicalDrives != nil && newOutput.Link.LogicalDrives.URL != "" { + logicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Link.LogicalDrives.URL, target, retryClient) + if err != nil { + log.Error("api call "+fqdn.String()+newOutput.Link.LogicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + continue + } - if err != nil { - log.Error("api call "+fqdn.String()+newOutput.Links.PhysicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) - continue + if logicalDriveOutput.MembersCount > 0 { + // loop through each Member in the "LogicalDrive" field + for _, member := range logicalDriveOutput.Members { + // append each URL in the Members array to the logicalDriveURLs array. + logicalDriveURLs = append(logicalDriveURLs, member.URL) + } + } } - if physicalDriveOutput.MembersCount > 0 { - for _, member := range physicalDriveOutput.Members { - physicalDriveURLs = append(physicalDriveURLs, member.URL) + + // If PhysicalDrives is present, parse physical drive endpoint until all urls are found + if newOutput.Link.PhysicalDrives != nil && newOutput.Link.PhysicalDrives.URL != "" { + physicalDriveOutput, err := getDriveEndpoint(fqdn.String()+newOutput.Link.PhysicalDrives.URL, target, retryClient) + if err != nil { + log.Error("api call "+fqdn.String()+newOutput.Links.PhysicalDrives.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + continue + } + + if physicalDriveOutput.MembersCount > 0 { + for _, member := range physicalDriveOutput.Members { + physicalDriveURLs = append(physicalDriveURLs, member.URL) + } } } } @@ -196,10 +232,12 @@ func NewExporter(ctx context.Context, target, uri, profile string) *Exporter { } // parse through "Links" to find "Drives" array - if len(chassisOutput.Links.Drives) > 0 { - // loop through drives array and append each odata.id url to nvmeDriveURLs list - for _, drive := range chassisOutput.Links.Drives { - nvmeDriveURLs = append(nvmeDriveURLs, drive.URL) + if chassisOutput.Links != nil { + if len(chassisOutput.Links.Drives) > 0 { + // loop through drives array and append each odata.id url to nvmeDriveURLs list + for _, drive := range chassisOutput.Links.Drives { + nvmeDriveURLs = append(nvmeDriveURLs, drive.URL) + } } } @@ -426,20 +464,33 @@ func (e *Exporter) exportPowerMetrics(body []byte) error { return fmt.Errorf("Error Unmarshalling DL360 PowerMetrics - " + err.Error()) } - for _, pc := range pm.PowerControl { - (*dlPower)["supplyTotalConsumed"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerConsumedWatts)) - (*dlPower)["supplyTotalCapacity"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerCapacityWatts)) + for idx, pc := range pm.PowerControl { + if pc.MemberID != "" { + (*dlPower)["supplyTotalConsumed"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerConsumedWatts)) + (*dlPower)["supplyTotalCapacity"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerCapacityWatts)) + } else { + (*dlPower)["supplyTotalConsumed"].WithLabelValues(strconv.Itoa(idx)).Set(float64(pc.PowerConsumedWatts)) + (*dlPower)["supplyTotalCapacity"].WithLabelValues(strconv.Itoa(idx)).Set(float64(pc.PowerCapacityWatts)) + } } for _, ps := range pm.PowerSupplies { if ps.Status.State == "Enabled" { - (*dlPower)["supplyOutput"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(float64(ps.LastPowerOutputWatts)) + if ps.MemberID != "" { + (*dlPower)["supplyOutput"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(float64(ps.LastPowerOutputWatts)) + } else { + (*dlPower)["supplyOutput"].WithLabelValues(strconv.Itoa(ps.Oem.Hp.BayNumber), ps.SparePartNumber).Set(float64(ps.LastPowerOutputWatts)) + } if ps.Status.Health == "OK" { state = OK } else { state = BAD } - (*dlPower)["supplyStatus"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(state) + if ps.MemberID != "" { + (*dlPower)["supplyStatus"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(state) + } else { + (*dlPower)["supplyStatus"].WithLabelValues(strconv.Itoa(ps.Oem.Hp.BayNumber), ps.SparePartNumber).Set(state) + } } } @@ -461,13 +512,21 @@ func (e *Exporter) exportThermalMetrics(body []byte) error { for _, fan := range tm.Fans { // Check fan status and convert string to numeric values if fan.Status.State == "Enabled" { - (*dlThermal)["fanSpeed"].WithLabelValues(fan.Name).Set(float64(fan.Reading)) + if fan.FanName != "" { + (*dlThermal)["fanSpeed"].WithLabelValues(fan.FanName).Set(float64(fan.CurrentReading)) + } else { + (*dlThermal)["fanSpeed"].WithLabelValues(fan.Name).Set(float64(fan.Reading)) + } if fan.Status.Health == "OK" { state = OK } else { state = BAD } - (*dlThermal)["fanStatus"].WithLabelValues(fan.Name).Set(state) + if fan.FanName != "" { + (*dlThermal)["fanStatus"].WithLabelValues(fan.FanName).Set(state) + } else { + (*dlThermal)["fanStatus"].WithLabelValues(fan.Name).Set(state) + } } } diff --git a/hpe/dl360/power.go b/hpe/dl360/power.go index 647a33d..69ba953 100644 --- a/hpe/dl360/power.go +++ b/hpe/dl360/power.go @@ -1,5 +1,5 @@ /* - * Copyright 2023 Comcast Cable Communications Management, LLC + * Copyright 2024 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,8 @@ type PowerSupply struct { // OemPower is the top level json object for historical data for wattage type OemPower struct { - Hpe Hpe `json:"Hpe"` + Hpe Hpe `json:"Hpe,omitempty"` + Hp Hpe `json:"Hp,omitempty"` } // Hpe contains metadata on power supply product info diff --git a/hpe/dl360/thermal.go b/hpe/dl360/thermal.go index b84aa8d..d63fbd6 100644 --- a/hpe/dl360/thermal.go +++ b/hpe/dl360/thermal.go @@ -1,5 +1,5 @@ /* - * Copyright 2023 Comcast Cable Communications Management, LLC + * Copyright 2024 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,13 @@ type ThermalMetrics struct { // Fan is the json object for a DL360 fan module type Fan struct { - MemberID string `json:"MemberId"` - Name string `json:"Name"` - Reading int `json:"Reading"` - ReadingUnits string `json:"ReadingUnits"` - Status StatusThermal `json:"Status"` + MemberID string `json:"MemberId"` + Name string `json:"Name"` + FanName string `json:"FanName"` + Reading int `json:"Reading"` + CurrentReading int `json:"CurrentReading"` + ReadingUnits string `json:"ReadingUnits"` + Status StatusThermal `json:"Status"` } // StatusThermal is the variable to determine if a fan or temperature sensor module is OK or not diff --git a/hpe/dl560/exporter.go b/hpe/dl560/exporter.go index 9ea334e..f34aec1 100644 --- a/hpe/dl560/exporter.go +++ b/hpe/dl560/exporter.go @@ -21,7 +21,7 @@ import ( "crypto/tls" "encoding/json" "fmt" - "io/ioutil" + "io" "net" "net/http" "net/url" @@ -46,7 +46,7 @@ const ( THERMAL = "ThermalMetrics" // POWER represents the power metric endpoint POWER = "PowerMetrics" - // DRIVE represents the logical drive metric endpoints + // DRIVE represents the physical drive metric endpoints DRIVE = "PhysicalDriveMetrics" // LOGICALDRIVE represents the Logical drive metric endpoint LOGICALDRIVE = "LogicalDriveMetrics" @@ -478,14 +478,14 @@ func getDriveEndpoints(url, host string, client *retryablehttp.Client) (GenericD } } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return drives, fmt.Errorf("Error reading Response Body - " + err.Error()) } err = json.Unmarshal(body, &drives) if err != nil { - return drives, fmt.Errorf("Error Unmarshalling S3260M5 Memory Collection struct - " + err.Error()) + return drives, fmt.Errorf("Error Unmarshalling DL560 Drive Collection struct - " + err.Error()) } return drives, nil diff --git a/hpe/xl420/drive.go b/hpe/xl420/drive.go new file mode 100644 index 0000000..abb358a --- /dev/null +++ b/hpe/xl420/drive.go @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +// /redfish/v1/Systems/1/SmartStorage/ArrayControllers/ + +type GenericDrive struct { + Members []struct { + URL string `json:"@odata.id"` + } `json:"Members"` + MembersCount int `json:"Members@odata.count,omitempty"` + Links *struct { + LogicalDrives *struct { + URL string `json:"href"` + } `json:"LogicalDrives,omitempty"` + PhysicalDrives *struct { + URL string `json:"href"` + } `json:"PhysicalDrives,omitempty"` + } `json:"Links,omitempty"` + Link *struct { + LogicalDrives *struct { + URL string `json:"href"` + } `json:"LogicalDrives,omitempty"` + PhysicalDrives *struct { + URL string `json:"href"` + } `json:"PhysicalDrives,omitempty"` + } `json:"links,omitempty"` +} + +// /redfish/v1/Systems/1/SmartStorage/ArrayControllers/X/LogicalDrives/X/ +type LogicalDriveMetrics struct { + ID string `json:"Id"` + CapacityMiB int `json:"CapacityMiB"` + Description string `json:"Description"` + InterfaceType string `json:"InterfaceType"` + LogicalDriveName string `json:"LogicalDriveName"` + LogicalDriveNumber int `json:"LogicalDriveNumber"` + Name string `json:"Name"` + Raid string `json:"Raid"` + Status Status `json:"Status"` + StripeSizeBytes int `json:"StripeSizeBytes"` +} + +// /redfish/v1/Systems/1/SmartStorage/ArrayControllers/X/DiskDrives/X/ +type PhysicalDriveMetrics struct { + ID string `json:"Id"` + CapacityGB int `json:"CapacityGB"` + Location string `json:"Location"` + Model string `json:"Model"` + Name string `json:"Name"` + SerialNumber string `json:"SerialNumber"` + Status Status `json:"Status"` +} diff --git a/hpe/xl420/exporter.go b/hpe/xl420/exporter.go new file mode 100644 index 0000000..f2f3b5c --- /dev/null +++ b/hpe/xl420/exporter.go @@ -0,0 +1,544 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/comcast/fishymetrics/common" + "github.com/comcast/fishymetrics/config" + "github.com/comcast/fishymetrics/pool" + "go.uber.org/zap" + + "github.com/hashicorp/go-retryablehttp" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + // XL420 is a HPE Hardware Device we scrape + XL420 = "XL420" + // THERMAL represents the thermal metric endpoint + THERMAL = "ThermalMetrics" + // POWER represents the power metric endpoint + POWER = "PowerMetrics" + // DRIVE represents the physical drive metric endpoints + DRIVE = "PhysicalDriveMetrics" + // LOGICALDRIVE represents the Logical drive metric endpoint + LOGICALDRIVE = "LogicalDriveMetrics" + // MEMORY represents the memory metric endpoints + MEMORY = "MemoryMetrics" + // OK is a string representation of the float 1.0 for device status + OK = 1.0 + // BAD is a string representation of the float 0.0 for device status + BAD = 0.0 + // DISABLED is a string representation of the float -1.0 for device status + DISABLED = -1.0 +) + +var ( + log *zap.Logger +) + +// Exporter collects chassis manager stats from the given URI and exports them using +// the prometheus metrics package. +type Exporter struct { + ctx context.Context + mutex sync.RWMutex + pool *pool.Pool + host string + credProfile string + + up prometheus.Gauge + deviceMetrics *map[string]*metrics +} + +// NewExporter returns an initialized Exporter for HPE XL420 device. +func NewExporter(ctx context.Context, target, uri, profile string) *Exporter { + var fqdn *url.URL + var tasks []*pool.Task + + log = zap.L() + + tr := &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 3 * time.Second, + }).Dial, + MaxIdleConns: 1, + MaxConnsPerHost: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: 90 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + TLSHandshakeTimeout: 10 * time.Second, + } + + retryClient := retryablehttp.NewClient() + retryClient.CheckRetry = retryablehttp.ErrorPropagatedRetryPolicy + retryClient.HTTPClient.Transport = tr + retryClient.HTTPClient.Timeout = 30 * time.Second + retryClient.Logger = nil + retryClient.RetryWaitMin = 2 * time.Second + retryClient.RetryWaitMax = 2 * time.Second + retryClient.RetryMax = 2 + retryClient.RequestLogHook = func(l retryablehttp.Logger, r *http.Request, i int) { + retryCount := i + if retryCount > 0 { + log.Error("api call "+r.URL.String()+" failed, retry #"+strconv.Itoa(retryCount), zap.Any("trace_id", ctx.Value("traceID"))) + } + } + + // Check that the target passed in has http:// or https:// prefixed + fqdn, err := url.ParseRequestURI(target) + if err != nil { + fqdn = &url.URL{ + Scheme: config.GetConfig().BMCScheme, + Host: target, + } + } + + tasks = append(tasks, + pool.NewTask(common.Fetch(fqdn.String()+uri+"/Chassis/1/Thermal/", THERMAL, target, profile, retryClient)), + pool.NewTask(common.Fetch(fqdn.String()+uri+"/Chassis/1/Power/", POWER, target, profile, retryClient)), + pool.NewTask(common.Fetch(fqdn.String()+uri+"/Systems/1/", MEMORY, target, profile, retryClient))) + + arrayControllers, err := getDriveEndpoints(fqdn.String()+uri+"/Systems/1/SmartStorage/ArrayControllers/", target, retryClient) + if err != nil { + log.Error("error when getting array controllers endpoint from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if arrayControllers.MembersCount > 0 { + for _, controller := range arrayControllers.Members { + getController, err := getDriveEndpoints(fqdn.String()+controller.URL, target, retryClient) + if err != nil { + log.Error("error when getting array controller from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if getController.Links != nil { + if getController.Links.LogicalDrives != nil && getController.Links.LogicalDrives.URL != "" { + logicalDrives, err := getDriveEndpoints(fqdn.String()+getController.Links.LogicalDrives.URL, target, retryClient) + if err != nil { + log.Error("error when getting logical drives endpoint from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if logicalDrives.MembersCount > 0 { + for _, logicalDrive := range logicalDrives.Members { + tasks = append(tasks, + pool.NewTask(common.Fetch(fqdn.String()+logicalDrive.URL, LOGICALDRIVE, target, profile, retryClient))) + } + } + } + + if getController.Links.PhysicalDrives != nil && getController.Links.PhysicalDrives.URL != "" { + physicalDrives, err := getDriveEndpoints(fqdn.String()+getController.Links.PhysicalDrives.URL, target, retryClient) + if err != nil { + log.Error("error when getting physical drives endpoint from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if physicalDrives.MembersCount > 0 { + for _, physicalDrive := range physicalDrives.Members { + tasks = append(tasks, + pool.NewTask(common.Fetch(fqdn.String()+physicalDrive.URL, DRIVE, target, profile, retryClient))) + } + } + } + + } else if getController.Link != nil { + if getController.Link.LogicalDrives != nil && getController.Link.LogicalDrives.URL != "" { + logicalDrives, err := getDriveEndpoints(fqdn.String()+getController.Link.LogicalDrives.URL, target, retryClient) + if err != nil { + log.Error("error when getting logical drives endpoint from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if logicalDrives.MembersCount > 0 { + for _, logicalDrive := range logicalDrives.Members { + tasks = append(tasks, + pool.NewTask(common.Fetch(fqdn.String()+logicalDrive.URL, LOGICALDRIVE, target, profile, retryClient))) + } + } + } + + if getController.Link.PhysicalDrives != nil && getController.Link.PhysicalDrives.URL != "" { + physicalDrives, err := getDriveEndpoints(fqdn.String()+getController.Link.PhysicalDrives.URL, target, retryClient) + if err != nil { + log.Error("error when getting physical drives endpoint from "+XL420, zap.Error(err), zap.Any("trace_id", ctx.Value("traceID"))) + return nil + } + + if physicalDrives.MembersCount > 0 { + for _, physicalDrive := range physicalDrives.Members { + tasks = append(tasks, + pool.NewTask(common.Fetch(fqdn.String()+physicalDrive.URL, DRIVE, target, profile, retryClient))) + } + } + } + } + } + } + + p := pool.NewPool(tasks, 1) + + // Create new map[string]*metrics for each new Exporter + metrx := NewDeviceMetrics() + + return &Exporter{ + ctx: ctx, + pool: p, + host: fqdn.Host, + credProfile: profile, + up: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "up", + Help: "Was the last scrape of chassis monitor successful.", + }), + deviceMetrics: metrx, + } +} + +// Describe describes all the metrics ever exported by the fishymetrics exporter. It +// implements prometheus.Collector. +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + for _, m := range *e.deviceMetrics { + for _, n := range *m { + n.Describe(ch) + } + } + ch <- e.up.Desc() +} + +// Collect fetches the stats from configured fishymetrics location and delivers them +// as Prometheus metrics. It implements prometheus.Collector. +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.mutex.Lock() // To protect metrics from concurrent collects. + defer e.mutex.Unlock() + + e.resetMetrics() + + // perform scrape if target is not on ignored list + if _, ok := common.IgnoredDevices[e.host]; !ok { + e.scrape() + } else { + e.up.Set(float64(2)) + } + + ch <- e.up + e.collectMetrics(ch) +} + +func (e *Exporter) resetMetrics() { + for _, m := range *e.deviceMetrics { + for _, n := range *m { + n.Reset() + } + } +} + +func (e *Exporter) collectMetrics(metrics chan<- prometheus.Metric) { + for _, m := range *e.deviceMetrics { + for _, n := range *m { + n.Collect(metrics) + } + } +} + +func (e *Exporter) scrape() { + + var result uint8 + state := uint8(1) + scrapes := len(e.pool.Tasks) + scrapeChan := make(chan uint8, scrapes) + + // Concurrently call the endpoints to help prevent reaching the maxiumum number of 4 simultaneous sessions + e.pool.Run() + for _, task := range e.pool.Tasks { + var err error + if task.Err != nil { + deviceState := uint8(0) + // If credentials are incorrect we will add host to be ignored until manual intervention + if strings.Contains(task.Err.Error(), "401") { + common.IgnoredDevices[e.host] = common.IgnoredDevice{ + Name: e.host, + Endpoint: "https://" + e.host + "/redfish/v1/Chassis", + Module: XL420, + CredentialProfile: e.credProfile, + } + log.Info("added host "+e.host+" to ignored list", zap.Any("trace_id", e.ctx.Value("traceID"))) + deviceState = 2 + } else { + deviceState = 0 + } + e.up.Set(float64(deviceState)) + log.Error("error from "+XL420, zap.Error(task.Err), zap.String("api", task.MetricType), zap.Any("trace_id", e.ctx.Value("traceID"))) + return + } + + switch task.MetricType { + case THERMAL: + err = e.exportThermalMetrics(task.Body) + case POWER: + err = e.exportPowerMetrics(task.Body) + case LOGICALDRIVE: + err = e.exportLogicalDriveMetrics(task.Body) + case DRIVE: + err = e.exportPhysicalDriveMetrics(task.Body) + case MEMORY: + err = e.exportMemoryMetrics(task.Body) + } + + if err != nil { + log.Error("error exporting metrics - from "+XL420, zap.Error(err), zap.String("api", task.MetricType), zap.Any("trace_id", e.ctx.Value("traceID"))) + continue + } + scrapeChan <- 1 + } + + // Get scrape results from goroutine(s) and perform bitwise AND, any failures should + // result in a scrape failure + for i := 0; i < scrapes; i++ { + result = <-scrapeChan + state &= result + } + + e.up.Set(float64(state)) + +} + +// exportPowerMetrics collects the XL420's power metrics in json format and sets the prometheus gauges +func (e *Exporter) exportPowerMetrics(body []byte) error { + + var state float64 + var pm PowerMetrics + var dlPower = (*e.deviceMetrics)["powerMetrics"] + err := json.Unmarshal(body, &pm) + if err != nil { + return fmt.Errorf("Error Unmarshalling XL420 PowerMetrics - " + err.Error()) + } + + for idx, pc := range pm.PowerControl { + if pc.MemberID != "" { + (*dlPower)["supplyTotalConsumed"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerConsumedWatts)) + (*dlPower)["supplyTotalCapacity"].WithLabelValues(pc.MemberID).Set(float64(pc.PowerCapacityWatts)) + } else { + (*dlPower)["supplyTotalConsumed"].WithLabelValues(strconv.Itoa(idx)).Set(float64(pc.PowerConsumedWatts)) + (*dlPower)["supplyTotalCapacity"].WithLabelValues(strconv.Itoa(idx)).Set(float64(pc.PowerCapacityWatts)) + } + } + + for _, ps := range pm.PowerSupplies { + if ps.Status.State == "Enabled" { + if ps.MemberID != "" { + (*dlPower)["supplyOutput"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(float64(ps.LastPowerOutputWatts)) + } else { + (*dlPower)["supplyOutput"].WithLabelValues(strconv.Itoa(ps.Oem.Hp.BayNumber), ps.SparePartNumber).Set(float64(ps.LastPowerOutputWatts)) + } + if ps.Status.Health == "OK" { + state = OK + } else { + state = BAD + } + if ps.MemberID != "" { + (*dlPower)["supplyStatus"].WithLabelValues(ps.MemberID, ps.SparePartNumber).Set(state) + } else { + (*dlPower)["supplyStatus"].WithLabelValues(strconv.Itoa(ps.Oem.Hp.BayNumber), ps.SparePartNumber).Set(state) + } + } + } + + return nil +} + +// exportThermalMetrics collects the XL420's thermal and fan metrics in json format and sets the prometheus gauges +func (e *Exporter) exportThermalMetrics(body []byte) error { + + var state float64 + var tm ThermalMetrics + var dlThermal = (*e.deviceMetrics)["thermalMetrics"] + err := json.Unmarshal(body, &tm) + if err != nil { + return fmt.Errorf("Error Unmarshalling XL420 ThermalMetrics - " + err.Error()) + } + + // Iterate through fans + for _, fan := range tm.Fans { + // Check fan status and convert string to numeric values + if fan.Status.State == "Enabled" { + if fan.FanName != "" { + (*dlThermal)["fanSpeed"].WithLabelValues(fan.FanName).Set(float64(fan.CurrentReading)) + } else { + (*dlThermal)["fanSpeed"].WithLabelValues(fan.Name).Set(float64(fan.Reading)) + } + if fan.Status.Health == "OK" { + state = OK + } else { + state = BAD + } + if fan.FanName != "" { + (*dlThermal)["fanStatus"].WithLabelValues(fan.FanName).Set(state) + } else { + (*dlThermal)["fanStatus"].WithLabelValues(fan.Name).Set(state) + } + } + } + + // Iterate through sensors + for _, sensor := range tm.Temperatures { + // Check sensor status and convert string to numeric values + if sensor.Status.State == "Enabled" { + (*dlThermal)["sensorTemperature"].WithLabelValues(strings.TrimRight(sensor.Name, " ")).Set(float64(sensor.ReadingCelsius)) + if sensor.Status.Health == "OK" { + state = OK + } else { + state = BAD + } + (*dlThermal)["sensorStatus"].WithLabelValues(strings.TrimRight(sensor.Name, " ")).Set(state) + } + } + + return nil +} + +// exportLogicalDriveMetrics collects the DL560 logical drive metrics in json format and sets the prometheus gauges +func (e *Exporter) exportLogicalDriveMetrics(body []byte) error { + + var state float64 + var dld LogicalDriveMetrics + var dlDrive = (*e.deviceMetrics)["logicalDriveMetrics"] + err := json.Unmarshal(body, &dld) + if err != nil { + return fmt.Errorf("Error Unmarshalling DL560 LogicalDriveMetrics - " + err.Error()) + } + // Check logical drive is enabled then check status and convert string to numeric values + if dld.Status.State == "Enabled" { + if dld.Status.Health == "OK" { + state = OK + } else { + state = BAD + } + } else { + state = DISABLED + } + + (*dlDrive)["logicalDriveStatus"].WithLabelValues(dld.Name, strconv.Itoa(dld.LogicalDriveNumber), dld.Raid).Set(state) + + return nil +} + +// exportPhysicalDriveMetrics collects the XL420 drive metrics in json format and sets the prometheus gauges +func (e *Exporter) exportPhysicalDriveMetrics(body []byte) error { + + var state float64 + var dpd PhysicalDriveMetrics + var dpDrive = (*e.deviceMetrics)["driveMetrics"] + err := json.Unmarshal(body, &dpd) + if err != nil { + return fmt.Errorf("Error Unmarshalling XL420 DriveMetrics - " + err.Error()) + } + // Check physical drive is enabled then check status and convert string to numeric values + if dpd.Status.State == "Enabled" { + if dpd.Status.Health == "OK" { + state = OK + } else { + state = BAD + } + } else { + state = DISABLED + } + + (*dpDrive)["physicalDriveStatus"].WithLabelValues(dpd.Name, dpd.ID, dpd.Location, dpd.SerialNumber).Set(state) + + return nil +} + +// exportMemoryMetrics collects the XL420 drive metrics in json format and sets the prometheus gauges +func (e *Exporter) exportMemoryMetrics(body []byte) error { + + var state float64 + var dlm MemoryMetrics + var dlMemory = (*e.deviceMetrics)["memoryMetrics"] + err := json.Unmarshal(body, &dlm) + if err != nil { + return fmt.Errorf("Error Unmarshalling XL420 MemoryMetrics - " + err.Error()) + } + // Check memory status and convert string to numeric values + if dlm.MemorySummary.Status.HealthRollup == "OK" { + state = OK + } else { + state = BAD + } + + (*dlMemory)["memoryStatus"].WithLabelValues(strconv.Itoa(dlm.MemorySummary.TotalSystemMemoryGiB)).Set(state) + + return nil +} + +func getDriveEndpoints(url, host string, client *retryablehttp.Client) (GenericDrive, error) { + var drives GenericDrive + var resp *http.Response + var err error + retryCount := 0 + req := common.BuildRequest(url, host) + + resp, err = common.DoRequest(client, req) + if err != nil { + return drives, err + } + defer resp.Body.Close() + if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) { + if resp.StatusCode == http.StatusNotFound { + for retryCount < 3 && resp.StatusCode == http.StatusNotFound { + time.Sleep(client.RetryWaitMin) + resp, err = common.DoRequest(client, req) + retryCount = retryCount + 1 + } + if err != nil { + return drives, err + } else if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) { + return drives, fmt.Errorf("HTTP status %d", resp.StatusCode) + } + } else { + return drives, fmt.Errorf("HTTP status %d", resp.StatusCode) + } + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return drives, fmt.Errorf("Error reading Response Body - " + err.Error()) + } + + err = json.Unmarshal(body, &drives) + if err != nil { + return drives, fmt.Errorf("Error Unmarshalling DL560 Drive Collection struct - " + err.Error()) + } + + return drives, nil +} diff --git a/hpe/xl420/memory.go b/hpe/xl420/memory.go new file mode 100644 index 0000000..e9511f7 --- /dev/null +++ b/hpe/xl420/memory.go @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +// /redfish/v1/systems/1/ + +// MemoryMetrics is the top level json object for XL420 Memory metadata +type MemoryMetrics struct { + ID string `json:"Id"` + MemorySummary MemorySummary `json:"MemorySummary"` +} + +// MemorySummary is the json object for XL420 MemorySummary metadata +type MemorySummary struct { + Status StatusMemory `json:"Status"` + TotalSystemMemoryGiB int `json:"TotalSystemMemoryGiB"` + TotalSystemPersistentMemoryGiB int `json:"TotalSystemPersistentMemoryGiB"` +} + +// StatusMemory is the variable to determine if the memory is OK or not +type StatusMemory struct { + HealthRollup string `json:"HealthRollup"` +} diff --git a/hpe/xl420/metrics.go b/hpe/xl420/metrics.go new file mode 100644 index 0000000..4f6dad8 --- /dev/null +++ b/hpe/xl420/metrics.go @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +type metrics map[string]*prometheus.GaugeVec + +func newServerMetric(metricName string, docString string, constLabels prometheus.Labels, labelNames []string) *prometheus.GaugeVec { + return prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: metricName, + Help: docString, + ConstLabels: constLabels, + }, + labelNames, + ) +} + +func NewDeviceMetrics() *map[string]*metrics { + var ( + ThermalMetrics = &metrics{ + "fanSpeed": newServerMetric("xl420_thermal_fan_speed", "Current fan speed in the unit of percentage, possible values are 0 - 100", nil, []string{"name"}), + "fanStatus": newServerMetric("xl420_thermal_fan_status", "Current fan status 1 = OK, 0 = BAD", nil, []string{"name"}), + "sensorTemperature": newServerMetric("xl420_thermal_sensor_temperature", "Current sensor temperature reading in Celsius", nil, []string{"name"}), + "sensorStatus": newServerMetric("xl420_thermal_sensor_status", "Current sensor status 1 = OK, 0 = BAD", nil, []string{"name"}), + } + + PowerMetrics = &metrics{ + "supplyOutput": newServerMetric("xl420_power_supply_output", "Power supply output in watts", nil, []string{"memberId", "sparePartNumber"}), + "supplyStatus": newServerMetric("xl420_power_supply_status", "Current power supply status 1 = OK, 0 = BAD", nil, []string{"memberId", "sparePartNumber"}), + "supplyTotalConsumed": newServerMetric("xl420_power_supply_total_consumed", "Total output of all power supplies in watts", nil, []string{"memberId"}), + "supplyTotalCapacity": newServerMetric("xl420_power_supply_total_capacity", "Total output capacity of all the power supplies", nil, []string{"memberId"}), + } + + LogicalDriveMetrics = &metrics{ + "logicalDriveStatus": newServerMetric("xl420_logical_drive_status", "Current logical drive status 1 = OK, 0 = BAD, -1 = DISABLED", nil, []string{"name", "logicalDriveNumber", "raid"}), + } + + PhysicalDriveMetrics = &metrics{ + "physicalDriveStatus": newServerMetric("xl420_physical_drive_status", "Current physical drive status 1 = OK, 0 = BAD, -1 = DISABLED", nil, []string{"name", "id", "location", "serialnumber"}), + } + + MemoryMetrics = &metrics{ + "memoryStatus": newServerMetric("xl420_memory_status", "Current memory status 1 = OK, 0 = BAD", nil, []string{"totalSystemMemoryGiB"}), + } + + Metrics = &map[string]*metrics{ + "thermalMetrics": ThermalMetrics, + "powerMetrics": PowerMetrics, + "logicalDriveMetrics": LogicalDriveMetrics, + "driveMetrics": PhysicalDriveMetrics, + "memoryMetrics": MemoryMetrics, + } + ) + + return Metrics +} diff --git a/hpe/xl420/network_adapter.go b/hpe/xl420/network_adapter.go new file mode 100644 index 0000000..f326931 --- /dev/null +++ b/hpe/xl420/network_adapter.go @@ -0,0 +1,65 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +// /redfish/v1/Systems/1/BaseNetworkAdapters + +// NetworkAdapter is the top level json object for XL420 Network Adapter metadata +type NetworkAdapter struct { + ID string `json:"Id"` + Firmware Firmware `json:"Firmware"` + Name string `json:"Name"` + PartNumber string `json:"PartNumber"` + PhysicalPorts []PhysicalPorts `json:"PhysicalPorts"` + SerialNumber string `json:"SerialNumber"` + StructuredName string `json:"StructuredName"` + Status Status `json:"Status"` + UEFIDevicePath string `json:"UEFIDevicePath"` +} + +// Firmware is the top level json object for XL420 Network Adapter metadata +type Firmware struct { + Current FirmwareCurrent `json:"Current"` +} + +// FirmwareCurrent contains the version in string format +type FirmwareCurrent struct { + Version string `json:"VersionString"` +} + +// PhysicalPorts contains the metadata for the Chassis NICs +type PhysicalPorts struct { + FullDuplex bool `json:"FullDuplex"` + IPv4Addresses []Addr `json:"IPv4Addresses"` + IPv6Addresses []Addr `json:"IPv6Addresses"` + LinkStatus string `json:"LinkStatus"` + MacAddress string `json:"MacAddress"` + Name string `json:"Name"` + SpeedMbps int `json:"SpeedMbps"` + Status Status `json:"Status"` +} + +// Addr contains the IPv4 or IPv6 Address in string format +type Addr struct { + Address string `json:"Address"` +} + +// Status contains metadata for the health of a particular component/module +type Status struct { + Health string `json:"Health"` + State string `json:"State,omitempty"` +} diff --git a/hpe/xl420/power.go b/hpe/xl420/power.go new file mode 100644 index 0000000..ce76ae5 --- /dev/null +++ b/hpe/xl420/power.go @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +// /redfish/v1/Chassis/1/Power/ + +// PowerMetrics is the top level json object for Power metadata +type PowerMetrics struct { + ID string `json:"Id"` + Name string `json:"Name"` + PowerControl []PowerControl `json:"PowerControl"` + PowerSupplies []PowerSupply `json:"PowerSupplies"` +} + +// PowerControl is the top level json object for metadata on power supply consumption +type PowerControl struct { + MemberID string `json:"MemberId"` + PowerCapacityWatts int `json:"PowerCapacityWatts"` + PowerConsumedWatts int `json:"PowerConsumedWatts"` + PowerMetrics PowerMetric `json:"PowerMetrics"` +} + +// PowerMetric contains avg/min/max power metadata +type PowerMetric struct { + AverageConsumedWatts int `json:"AverageConsumedWatts"` + IntervalInMin int `json:"IntervalInMin"` + MaxConsumedWatts int `json:"MaxConsumedWatts"` + MinConsumedWatts int `json:"MinConsumedWatts"` +} + +// PowerSupply is the top level json object for metadata on power supply product info +type PowerSupply struct { + FirmwareVersion string `json:"FirmwareVersion"` + LastPowerOutputWatts int `json:"LastPowerOutputWatts"` + LineInputVoltage int `json:"LineInputVoltage"` + LineInputVoltageType string `json:"LineInputVoltageType"` + Manufacturer string `json:"Manufacturer"` + MemberID string `json:"MemberId"` + Model string `json:"Model"` + Name string `json:"Name"` + Oem OemPower `json:"Oem"` + PowerCapacityWatts int `json:"PowerCapacityWatts"` + PowerSupplyType string `json:"PowerSupplyType"` + SerialNumber string `json:"SerialNumber"` + SparePartNumber string `json:"SparePartNumber"` + Status Status `json:"Status"` +} + +// OemPower is the top level json object for historical data for wattage +type OemPower struct { + Hpe Hpe `json:"Hpe"` + Hp Hpe `json:"Hp"` +} + +// Hpe contains metadata on power supply product info +type Hpe struct { + AveragePowerOutputWatts int `json:"AveragePowerOutputWatts"` + BayNumber int `json:"BayNumber"` + HotplugCapable bool `json:"HotplugCapable"` + MaxPowerOutputWatts int `json:"MaxPowerOutputWatts"` + Mismatched bool `json:"Mismatched"` + PowerSupplyStatus Status `json:"PowerSupplyStatus"` + IPDUCapable bool `json:"iPDUCapable"` +} diff --git a/hpe/xl420/thermal.go b/hpe/xl420/thermal.go new file mode 100644 index 0000000..c3151aa --- /dev/null +++ b/hpe/xl420/thermal.go @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xl420 + +// /redfish/v1/Chassis/1/Thermal/ + +// ThermalMetrics is the top level json object for XL420 Thermal metadata +type ThermalMetrics struct { + ID string `json:"Id"` + Fans []Fan `json:"Fans"` + Name string `json:"Name"` + Temperatures []Temperature `json:"Temperatures"` +} + +// Fan is the json object for a XL420 fan module +type Fan struct { + MemberID string `json:"MemberId"` + Name string `json:"Name"` + FanName string `json:"FanName"` + Reading int `json:"Reading"` + CurrentReading int `json:"CurrentReading"` + ReadingUnits string `json:"ReadingUnits"` + Status StatusThermal `json:"Status"` +} + +// StatusThermal is the variable to determine if a fan or temperature sensor module is OK or not +type StatusThermal struct { + Health string `json:"Health"` + State string `json:"State"` +} + +// Temperature is the json object for a XL420 temperature sensor module +type Temperature struct { + MemberID string `json:"MemberId"` + Name string `json:"Name"` + PhysicalContext string `json:"PhysicalContext"` + ReadingCelsius int `json:"ReadingCelsius"` + SensorNumber int `json:"SensorNumber"` + Status StatusThermal `json:"Status"` + UpperThresholdCritical int `json:"UpperThresholdCritical"` + UpperThresholdFatal int `json:"UpperThresholdFatal"` +}