diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85c60e4..2b49191 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ project adheres to [Semantic Versioning](http://semver.org/) and this change
log is based on the [Keep a CHANGELOG](http://keepachangelog.com/) project.
## Unreleased
+- Added HPE DL380 Gen10 support
+- Enhanced drive metrics collection for DL380 model servers to include NVME, Storage Disk Drives, and Logical Drives
## Fixed
- Cisco UCS C220 - add additional edge cases when collecting memory metrics [#2](https://github.com/Comcast/fishymetrics/issues/2)
diff --git a/README.md b/README.md
index 5dd840f..812b4e3 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@ exports them via HTTP for Prometheus consumption.
Current device models supported
- HP Moonshot
+- HP DL380
- HP DL360
- HP DL20
- Cisco UCS C220 M5
@@ -131,7 +132,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", "c220", "s3260m4", "s3260m5"]`
+done with relabelling. available module options `["moonshot", "dl360", "dl20", "dl380", "c220", "s3260m4", "s3260m5"]`
Example config:
```YAML
diff --git a/cmd/fishymetrics/main.go b/cmd/fishymetrics/main.go
index c3dc4aa..3a6b5ee 100644
--- a/cmd/fishymetrics/main.go
+++ b/cmd/fishymetrics/main.go
@@ -40,6 +40,7 @@ import (
"github.com/comcast/fishymetrics/config"
"github.com/comcast/fishymetrics/hpe/dl20"
"github.com/comcast/fishymetrics/hpe/dl360"
+ "github.com/comcast/fishymetrics/hpe/dl380"
"github.com/comcast/fishymetrics/hpe/moonshot"
"github.com/comcast/fishymetrics/logger"
"github.com/comcast/fishymetrics/middleware/muxprom"
@@ -138,6 +139,8 @@ func handler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
switch moduleName {
case "moonshot":
exporter = moonshot.NewExporter(r.Context(), target, uri)
+ case "dl380":
+ exporter = dl380.NewExporter(r.Context(), target, uri)
case "dl360":
exporter = dl360.NewExporter(r.Context(), target, uri)
case "dl20":
@@ -150,7 +153,7 @@ func handler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
exporter, err = s3260m5.NewExporter(r.Context(), target, uri)
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, dl20, c220, s3260m4, s3260m5]", http.StatusBadRequest)
+ http.Error(w, "'module' parameter does not match available options: [moonshot, dl360, dl380, dl20, c220, s3260m4, s3260m5]", http.StatusBadRequest)
return
}
diff --git a/cmd/fishymetrics/templates.go b/cmd/fishymetrics/templates.go
index 485e767..fbb7b40 100644
--- a/cmd/fishymetrics/templates.go
+++ b/cmd/fishymetrics/templates.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.
@@ -56,6 +56,7 @@ const indexTmpl string = `
Module:
moonshot
+ dl380
dl360
dl20
c220
diff --git a/hpe/dl380/drive.go b/hpe/dl380/drive.go
new file mode 100644
index 0000000..a0e34c6
--- /dev/null
+++ b/hpe/dl380/drive.go
@@ -0,0 +1,107 @@
+/*
+ * 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 dl380
+
+// NVME's
+// /redfish/v1/chassis/1/
+// NVMeMetrics is the top level json object for DL380 NVMe Metrics Metadata
+type NVMeDriveMetrics struct {
+ ID string `json:"Id"`
+ Model string `json:"Model"`
+ Name string `json:"Name"`
+ MediaType string `json:"MediaType"`
+ PhysicalLocation PhysicalLocation `json:"PhysicalLocation"`
+ Protocol string `json:"Protocol"`
+ Status DriveStatus `json:"Status"`
+ FailurePredicted bool `json:"FailurePredicted"`
+ CapacityBytes int `json:"CapacityBytes"`
+}
+
+// Logical Drives
+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 DriveStatus `json:"Status"`
+ StripeSizebytes int `json:"StripeSizebytes"`
+ VolumeUniqueIdentifier string `json:"VolumeUniqueIdentifier"`
+}
+
+// Disk Drives
+type DiskDriveMetrics struct {
+ Id string `json:"Id"`
+ CapacityMiB int `json:"CapacityMiB"`
+ Description string `json:"Description"`
+ InterfaceType string `json:"InterfaceType"`
+ Name string `json:"Name"`
+ Model string `json:"Model"`
+ Status DriveStatus `json:"Status"`
+ Location string `json:"Location"`
+ SerialNumber string `json:"SerialNumber"`
+}
+
+// NVME, Logical, and Physical Disk Drive Status
+type DriveStatus struct {
+ Health string `json:"Health,omitempty"`
+ State string `json:"Enabled,omitempty"`
+}
+
+// GenericDrive is used to iterate over differing drive endpoints
+type GenericDrive struct {
+ Members []struct {
+ URL string `json:"@odata.id"`
+ } `json:"Members,omitempty"`
+ Links struct {
+ Drives []struct {
+ URL string `json:"@odata.id"`
+ } `json:"Drives,omitempty"`
+ LogicalDrives struct {
+ URL string `json:"@odata.id"`
+ } `json:"LogicalDrives,omitempty"`
+ PhysicalDrives struct {
+ URL string `json:"@odata.id"`
+ } `json:"PhysicalDrives,omitempty"`
+ } `json:"Links,omitempty"`
+ MembersCount int `json:"Members@odata.count,omitempty"`
+}
+
+// PhysicalLocation
+type PhysicalLocation struct {
+ PartLocation PartLocation `json:"PartLocation"`
+}
+
+// PartLocation is a variable that determines the Box and the Bay location of the NVMe drive
+type PartLocation struct {
+ ServiceLabel string `json:"ServiceLabel"`
+}
+
+// Contents of Oem
+type Oem struct {
+ Hpe HpeCont `json:"Hpe"`
+}
+
+// Contents of Hpe
+type HpeCont struct {
+ CurrentTemperatureCelsius int `json:"CurrentTemperatureCelsius"`
+ DriveStatus DriveStatus `json:"Status"`
+ NVMeID string `json:"NVMeId"`
+}
diff --git a/hpe/dl380/exporter.go b/hpe/dl380/exporter.go
new file mode 100644
index 0000000..8f6f1d9
--- /dev/null
+++ b/hpe/dl380/exporter.go
@@ -0,0 +1,553 @@
+/*
+ * 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 dl380
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "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 (
+ // DL380 is a HPE Hardware Device we scrape
+ DL380 = "DL380"
+ // THERMAL represents the thermal metric endpoint
+ THERMAL = "ThermalMetrics"
+ // POWER represents the power metric endpoint
+ POWER = "PowerMetrics"
+ // NVME represents the NVMe drive metric endpoint
+ NVME = "NVMeDriveMetrics"
+ // DISKDRIVE represents the Disk Drive metric endpoints
+ DISKDRIVE = "DiskDriveMetrics"
+ // 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
+
+ up prometheus.Gauge
+ deviceMetrics *map[string]*metrics
+}
+
+// NewExporter returns an initialized Exporter for HPE DL380 device.
+func NewExporter(ctx context.Context, target, uri 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().OOBScheme,
+ Host: target,
+ }
+ }
+
+ // vars for drive parsing
+ var (
+ initialURL = "/Systems/1/SmartStorage/ArrayControllers"
+ url = initialURL
+ chassisUrl = "/Chassis/1"
+ logicalDriveURLs []string
+ physicalDriveURLs []string
+ nvmeDriveURLs []string
+ )
+
+ // PARSING DRIVE ENDPOINTS
+ // Get initial JSON return of /redfish/v1/Systems/1/SmartStorage/ArrayControllers/ set to output
+ output, err := getDriveEndpoint(fqdn.String()+uri+url, target, retryClient)
+
+ // Loop through Members to get ArrayController URLs
+ if err != nil {
+ log.Error("api call "+fqdn.String()+uri+url+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID")))
+ return nil
+ }
+
+ if output.MembersCount > 0 {
+ for _, member := range output.Members {
+ // for each ArrayController URL, get the JSON object
+ newOutput, err := getDriveEndpoint(fqdn.String()+member.URL, target, retryClient)
+ if err != nil {
+ log.Error("api call "+fqdn.String()+member.URL+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID")))
+ 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 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.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)
+ }
+ }
+ }
+ }
+ }
+
+ // parse to find NVME drives
+ chassisOutput, err := getDriveEndpoint(fqdn.String()+uri+chassisUrl, target, retryClient)
+ if err != nil {
+ log.Error("api call "+fqdn.String()+uri+chassisUrl+" failed - ", zap.Error(err), zap.Any("trace_id", ctx.Value("traceID")))
+ return nil
+ }
+
+ // 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)
+ }
+ }
+
+ // Loop through logicalDriveURLs, physicalDriveURLs, and nvmeDriveURLs and append each URL to the tasks pool
+ for _, url := range logicalDriveURLs {
+ tasks = append(tasks, pool.NewTask(common.Fetch(fqdn.String()+url, LOGICALDRIVE, target, retryClient)))
+ }
+
+ for _, url := range physicalDriveURLs {
+ tasks = append(tasks, pool.NewTask(common.Fetch(fqdn.String()+url, DISKDRIVE, target, retryClient)))
+ }
+
+ for _, url := range nvmeDriveURLs {
+ tasks = append(tasks, pool.NewTask(common.Fetch(fqdn.String()+url, NVME, target, retryClient)))
+ }
+
+ // Additional tasks for pool to perform
+ tasks = append(tasks,
+ pool.NewTask(common.Fetch(fqdn.String()+uri+"/Chassis/1/Thermal", THERMAL, target, retryClient)),
+ pool.NewTask(common.Fetch(fqdn.String()+uri+"/Chassis/1/Power", POWER, target, retryClient)),
+ pool.NewTask(common.Fetch(fqdn.String()+uri+"/Systems/1", MEMORY, target, retryClient)))
+
+ // Prepare the pool of tasks
+ 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,
+ 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: DL380,
+ }
+ 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 "+DL380, 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 NVME:
+ err = e.exportNVMeDriveMetrics(task.Body)
+ case DISKDRIVE:
+ err = e.exportPhysicalDriveMetrics(task.Body)
+ case LOGICALDRIVE:
+ err = e.exportLogicalDriveMetrics(task.Body)
+ case MEMORY:
+ err = e.exportMemoryMetrics(task.Body)
+ }
+
+ if err != nil {
+ log.Error("error exporting metrics - from "+DL380, 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))
+
+}
+
+// exportPhysicalDriveMetrics collects the DL380's physical drive metrics in json format and sets the prometheus gauges
+func (e *Exporter) exportPhysicalDriveMetrics(body []byte) error {
+
+ var state float64
+ var dlphysical DiskDriveMetrics
+ var dlphysicaldrive = (*e.deviceMetrics)["diskDriveMetrics"]
+ err := json.Unmarshal(body, &dlphysical)
+ if err != nil {
+ return fmt.Errorf("Error Unmarshalling DL380 DiskDriveMetrics - " + err.Error())
+ }
+ // Check physical drive is enabled then check status and convert string to numeric values
+
+ if dlphysical.Status.Health == "OK" {
+ state = OK
+ } else {
+ state = BAD
+ }
+
+ // Physical drives need to have a unique identifier like location so as to not overwrite data
+ // physical drives can have the same ID, but belong to a different ArrayController, therefore need more than just the ID as a unique identifier.
+ (*dlphysicaldrive)["driveStatus"].WithLabelValues(dlphysical.Name, dlphysical.Id, dlphysical.Location).Set(state)
+ return nil
+}
+
+// exportLogicalDriveMetrics collects the DL380's physical drive metrics in json format and sets the prometheus gauges
+func (e *Exporter) exportLogicalDriveMetrics(body []byte) error {
+ var state float64
+ var dllogical LogicalDriveMetrics
+ var dllogicaldrive = (*e.deviceMetrics)["logicalDriveMetrics"]
+ err := json.Unmarshal(body, &dllogical)
+ if err != nil {
+ return fmt.Errorf("Error Unmarshalling DL380 LogicalDriveMetrics - " + err.Error())
+ }
+ // Check physical drive is enabled then check status and convert string to numeric values
+ if dllogical.Status.Health == "OK" {
+ state = OK
+ } else {
+ state = BAD
+ }
+
+ (*dllogicaldrive)["raidStatus"].WithLabelValues(dllogical.Name, dllogical.LogicalDriveName, dllogical.VolumeUniqueIdentifier, dllogical.Raid).Set(state)
+ return nil
+}
+
+// exportNVMeDriveMetrics collects the DL380 NVME drive metrics in json format and sets the prometheus gauges
+func (e *Exporter) exportNVMeDriveMetrics(body []byte) error {
+ var state float64
+ var dlnvme NVMeDriveMetrics
+ var dlnvmedrive = (*e.deviceMetrics)["nvmeMetrics"]
+ err := json.Unmarshal(body, &dlnvme)
+ if err != nil {
+ return fmt.Errorf("Error Unmarshalling DL380 NVMeDriveMetrics - " + err.Error())
+ }
+
+ // Check nvme drive is enabled then check status and convert string to numeric values
+ if dlnvme.Status.Health == "OK" {
+ state = OK
+ } else {
+ state = BAD
+ }
+
+ (*dlnvmedrive)["nvmeDriveStatus"].WithLabelValues(dlnvme.Protocol, dlnvme.ID, dlnvme.PhysicalLocation.PartLocation.ServiceLabel).Set(state)
+ return nil
+}
+
+// exportPowerMetrics collects the DL380'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 DL380 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 _, ps := range pm.PowerSupplies {
+ if ps.Status.State == "Enabled" {
+ (*dlPower)["supplyOutput"].WithLabelValues(ps.MemberID, 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)
+ }
+ }
+
+ return nil
+}
+
+// exportThermalMetrics collects the DL380'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 DL380 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" {
+ (*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)
+ }
+ }
+
+ // 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
+}
+
+// exportMemoryMetrics collects the DL380 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 DL380 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
+}
+
+// The getDriveEndpoint function is used in a recursive fashion to get the body response
+// of any type of drive, NVMe, Physical DiskDrives, or Logical Drives, using the GenericDrive struct
+// This is used to find the final drive endpoints to append to the task pool for final scraping.
+func getDriveEndpoint(url, host string, client *retryablehttp.Client) (GenericDrive, error) {
+ var drive 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 drive, 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 drive, err
+ } else if !(resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices) {
+ return drive, fmt.Errorf("HTTP status %d", resp.StatusCode)
+ }
+ } else {
+ return drive, fmt.Errorf("HTTP status %d", resp.StatusCode)
+ }
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return drive, fmt.Errorf("Error reading Response Body - " + err.Error())
+ }
+
+ err = json.Unmarshal(body, &drive)
+ if err != nil {
+ return drive, fmt.Errorf("Error Unmarshalling DL380 drive struct - " + err.Error())
+ }
+
+ return drive, nil
+}
diff --git a/hpe/dl380/memory.go b/hpe/dl380/memory.go
new file mode 100644
index 0000000..ce443cf
--- /dev/null
+++ b/hpe/dl380/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 dl380
+
+// /redfish/v1/systems/1/
+
+// MemoryMetrics is the top level json object for DL380 Memory metadata
+type MemoryMetrics struct {
+ ID string `json:"Id"`
+ MemorySummary MemorySummary `json:"MemorySummary"`
+}
+
+// MemorySummary is the json object for DL380 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/dl380/metrics.go b/hpe/dl380/metrics.go
new file mode 100644
index 0000000..dd9dec5
--- /dev/null
+++ b/hpe/dl380/metrics.go
@@ -0,0 +1,82 @@
+/*
+ * 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 dl380
+
+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("dl380_thermal_fan_speed", "Current fan speed in the unit of percentage, possible values are 0 - 100", nil, []string{"name"}),
+ "fanStatus": newServerMetric("dl380_thermal_fan_status", "Current fan status 1 = OK, 0 = BAD", nil, []string{"name"}),
+ "sensorTemperature": newServerMetric("dl380_thermal_sensor_temperature", "Current sensor temperature reading in Celsius", nil, []string{"name"}),
+ "sensorStatus": newServerMetric("dl380_thermal_sensor_status", "Current sensor status 1 = OK, 0 = BAD", nil, []string{"name"}),
+ }
+
+ PowerMetrics = &metrics{
+ "supplyOutput": newServerMetric("dl380_power_supply_output", "Power supply output in watts", nil, []string{"memberId", "sparePartNumber"}),
+ "supplyStatus": newServerMetric("dl380_power_supply_status", "Current power supply status 1 = OK, 0 = BAD", nil, []string{"memberId", "sparePartNumber"}),
+ "supplyTotalConsumed": newServerMetric("dl380_power_supply_total_consumed", "Total output of all power supplies in watts", nil, []string{"memberId"}),
+ "supplyTotalCapacity": newServerMetric("dl380_power_supply_total_capacity", "Total output capacity of all the power supplies", nil, []string{"memberId"}),
+ }
+
+ // Splitting out the three different types of drives to gather metrics on each (NVMe, Disk Drive, and Logical Drive)
+ // NVMe Drive Metrics
+ NVMeDriveMetrics = &metrics{
+ "nvmeDriveStatus": newServerMetric("dl380_nvme_drive_status", "Current NVME status 1 = OK, 0 = BAD", nil, []string{"protocol", "id", "serviceLabel"}),
+ }
+
+ // Phyiscal Storage Disk Drive Metrics
+ DiskDriveMetrics = &metrics{
+ "driveStatus": newServerMetric("dl380_disk_drive_status", "Current Disk Drive status 1 = OK, 0 = BAD", nil, []string{"name", "Id", "location"}), // DiskDriveStatus values
+ }
+
+ // Logical Disk Drive Metrics
+ LogicalDriveMetrics = &metrics{
+ "raidStatus": newServerMetric("dl380_logical_drive_raid", "Current Logical Drive Raid", nil, []string{"name", "logicaldrivename", "volumeuniqueidentifier", "raid"}), // Logical Drive Raid value
+ }
+
+ MemoryMetrics = &metrics{
+ "memoryStatus": newServerMetric("dl380_memory_status", "Current memory status 1 = OK, 0 = BAD", nil, []string{"totalSystemMemoryGiB"}),
+ }
+
+ Metrics = &map[string]*metrics{
+ "thermalMetrics": ThermalMetrics,
+ "powerMetrics": PowerMetrics,
+ "nvmeMetrics": NVMeDriveMetrics,
+ "diskDriveMetrics": DiskDriveMetrics,
+ "logicalDriveMetrics": LogicalDriveMetrics,
+ "memoryMetrics": MemoryMetrics,
+ }
+ )
+ return Metrics
+}
diff --git a/hpe/dl380/network_adapter.go b/hpe/dl380/network_adapter.go
new file mode 100644
index 0000000..ea778ac
--- /dev/null
+++ b/hpe/dl380/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 dl380
+
+// /redfish/v1/Systems/1/BaseNetworkAdapters
+
+// NetworkAdapter is the top level json object for DL380 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 DL380 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/dl380/power.go b/hpe/dl380/power.go
new file mode 100644
index 0000000..3d6b343
--- /dev/null
+++ b/hpe/dl380/power.go
@@ -0,0 +1,77 @@
+/*
+ * 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 dl380
+
+// /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"`
+}
+
+// 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/dl380/thermal.go b/hpe/dl380/thermal.go
new file mode 100644
index 0000000..ff1eb86
--- /dev/null
+++ b/hpe/dl380/thermal.go
@@ -0,0 +1,54 @@
+/*
+ * 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 dl380
+
+// /redfish/v1/Chassis/1/Thermal/
+
+// ThermalMetrics is the top level json object for DL380 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 DL380 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"`
+}
+
+// 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 DL380 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"`
+}