summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorIlya Mashchenko <ilya@netdata.cloud>2024-05-14 10:59:25 +0300
committerGitHub <noreply@github.com>2024-05-14 10:59:25 +0300
commite8618460d1ac4c26592b983f2c88628aea0d82b1 (patch)
tree826deb1f89b41d3c5d9784e29c9f2603c098a171 /src
parent519467f4ece870b516772f0fef5af8661ef118f6 (diff)
go.d hpssa (#17637)
Diffstat (limited to 'src')
-rw-r--r--src/go/collectors/go.d.plugin/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d.conf1
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d/hpssa.conf5
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/charts.go403
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/collect.go139
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/config_schema.json35
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/exec.go46
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/hpssa.go110
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/hpssa_test.go430
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/init.go23
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/metadata.yaml213
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/parse.go364
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/testdata/config.json4
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/testdata/config.yaml2
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/testdata/ssacli-P212_P410i.txt748
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/testdata/ssacli-P400ar.txt397
-rw-r--r--src/go/collectors/go.d.plugin/modules/hpssa/testdata/ssacli-P400i-unassigned.txt207
-rw-r--r--src/go/collectors/go.d.plugin/modules/init.go1
18 files changed, 3129 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/README.md b/src/go/collectors/go.d.plugin/README.md
index 4d0718fc0c..36a0ce6721 100644
--- a/src/go/collectors/go.d.plugin/README.md
+++ b/src/go/collectors/go.d.plugin/README.md
@@ -79,6 +79,7 @@ see the appropriate collector readme.
| [haproxy](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/haproxy) | HAProxy |
| [hddtemp](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hddtemp) | Disks temperature |
| [hdfs](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hdfs) | HDFS |
+| [hpssa](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hpssa) | HPE Smart Array |
| [httpcheck](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/httpcheck) | Any HTTP Endpoint |
| [intelgpu](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/intelgpu) | Intel integrated GPU |
| [isc_dhcpd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/isc_dhcpd) | ISC DHCP |
diff --git a/src/go/collectors/go.d.plugin/config/go.d.conf b/src/go/collectors/go.d.plugin/config/go.d.conf
index 34abed37f4..faee39a8ca 100644
--- a/src/go/collectors/go.d.plugin/config/go.d.conf
+++ b/src/go/collectors/go.d.plugin/config/go.d.conf
@@ -42,6 +42,7 @@ modules:
# haproxy: yes
# hddtemp: yes
# hdfs: yes
+# hpssa: yes
# httpcheck: yes
# intelgpu: yes
# isc_dhcpd: yes
diff --git a/src/go/collectors/go.d.plugin/config/go.d/hpssa.conf b/src/go/collectors/go.d.plugin/config/go.d/hpssa.conf
new file mode 100644
index 0000000000..c5abeb4868
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/config/go.d/hpssa.conf
@@ -0,0 +1,5 @@
+## All available configuration options, their descriptions and default values:
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/hpssa#readme
+
+jobs:
+ - name: hpssa
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/charts.go b/src/go/collectors/go.d.plugin/modules/hpssa/charts.go
new file mode 100644
index 0000000000..324215951a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/charts.go
@@ -0,0 +1,403 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package hpssa
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioControllerStatus = module.Priority + iota
+ prioControllerTemperature
+
+ prioControllerCacheModulePresenceStatus
+ prioControllerCacheModuleStatus
+ prioControllerCacheModuleTemperature
+ prioControllerCacheModuleBatteryStatus
+
+ prioArrayStatus
+
+ prioLogicalDriveStatus
+
+ prioPhysicalDriveStatus
+ prioPhysicalDriveTemperature
+)
+
+var controllerChartsTmpl = module.Charts{
+ controllerStatusChartTmpl.Copy(),
+ controllerTemperatureChartTmpl.Copy(),
+
+ controllerCacheModulePresenceStatusChartTmpl.Copy(),
+ controllerCacheModuleStatusChartTmpl.Copy(),
+ controllerCacheModuleTemperatureChartTmpl.Copy(),
+ controllerCacheModuleBatteryStatusChartTmpl.Copy(),
+}
+
+var (
+ controllerStatusChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_status",
+ Title: "Controller status",
+ Units: "status",
+ Fam: "controllers",
+ Ctx: "hpssa.controller_status",
+ Type: module.Line,
+ Priority: prioControllerStatus,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_status_ok", Name: "ok"},
+ {ID: "cntrl_%s_slot_%s_status_nok", Name: "nok"},
+ },
+ }
+ controllerTemperatureChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_temperature",
+ Title: "Controller temperature",
+ Units: "Celsius",
+ Fam: "controllers",
+ Ctx: "hpssa.controller_temperature",
+ Type: module.Line,
+ Priority: prioControllerTemperature,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_temperature", Name: "temperature"},
+ },
+ }
+
+ controllerCacheModulePresenceStatusChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_cache_presence_status",
+ Title: "Controller cache module presence",
+ Units: "status",
+ Fam: "cache",
+ Ctx: "hpssa.controller_cache_module_presence_status",
+ Type: module.Line,
+ Priority: prioControllerCacheModulePresenceStatus,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_cache_presence_status_present", Name: "present"},
+ {ID: "cntrl_%s_slot_%s_cache_presence_status_not_present", Name: "not_present"},
+ },
+ }
+ controllerCacheModuleStatusChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_cache_status",
+ Title: "Controller cache module status",
+ Units: "status",
+ Fam: "cache",
+ Ctx: "hpssa.controller_cache_module_status",
+ Type: module.Line,
+ Priority: prioControllerCacheModuleStatus,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_cache_status_ok", Name: "ok"},
+ {ID: "cntrl_%s_slot_%s_cache_status_nok", Name: "nok"},
+ },
+ }
+ controllerCacheModuleTemperatureChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_cache_temperature",
+ Title: "Controller cache module temperature",
+ Units: "Celsius",
+ Fam: "cache",
+ Ctx: "hpssa.controller_cache_module_temperature",
+ Type: module.Line,
+ Priority: prioControllerCacheModuleTemperature,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_cache_temperature", Name: "temperature"},
+ },
+ }
+ controllerCacheModuleBatteryStatusChartTmpl = module.Chart{
+ ID: "cntrl_%s_slot_%s_cache_battery_status",
+ Title: "Controller cache module battery status",
+ Units: "status",
+ Fam: "cache",
+ Ctx: "hpssa.controller_cache_module_battery status",
+ Type: module.Line,
+ Priority: prioControllerCacheModuleBatteryStatus,
+ Dims: module.Dims{
+ {ID: "cntrl_%s_slot_%s_cache_battery_status_ok", Name: "ok"},
+ {ID: "cntrl_%s_slot_%s_cache_battery_status_nok", Name: "nok"},
+ },
+ }
+)
+
+var arrayChartsTmpl = module.Charts{
+ arrayStatusChartTmpl.Copy(),
+}
+
+var (
+ arrayStatusChartTmpl = module.Chart{
+ ID: "array_%s_cntrl_%s_slot_%s_status",
+ Title: "Array status",
+ Units: "status",
+ Fam: "arrays",
+ Ctx: "hpssa.array_status",
+ Type: module.Line,
+ Priority: prioArrayStatus,
+ Dims: module.Dims{
+ {ID: "array_%s_cntrl_%s_slot_%s_status_ok", Name: "ok"},
+ {ID: "array_%s_cntrl_%s_slot_%s_status_nok", Name: "nok"},
+ },
+ }
+)
+
+var logicalDriveChartsTmpl = module.Charts{
+ logicalDriveStatusChartTmpl.Copy(),
+}
+
+var (
+ logicalDriveStatusChartTmpl = module.Chart{
+ ID: "ld_%s_array_%s_cntrl_%s_slot_%s_status",
+ Title: "Logical Drive status",
+ Units: "status",
+ Fam: "logical drives",
+ Ctx: "hpssa.logical_drive_status",
+ Type: module.Line,
+ Priority: prioLogicalDriveStatus,
+ Dims: module.Dims{
+ {ID: "ld_%s_array_%s_cntrl_%s_slot_%s_status_ok", Name: "ok"},
+ {ID: "ld_%s_array_%s_cntrl_%s_slot_%s_status_nok", Name: "nok"},
+ },
+ }
+)
+
+var physicalDriveChartsTmpl = module.Charts{
+ physicalDriveStatusChartTmpl.Copy(),
+ physicalDriveTemperatureChartTmpl.Copy(),
+}
+
+var (
+ physicalDriveStatusChartTmpl = module.Chart{
+ ID: "pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_status",
+ Title: "Physical Drive status",
+ Units: "status",
+ Fam: "physical drives",
+ Ctx: "hpssa.physical_drive_status",
+ Type: module.Line,
+ Priority: prioPhysicalDriveStatus,
+ Dims: module.Dims{
+ {ID: "pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_status_ok", Name: "ok"},
+ {ID: "pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_status_nok", Name: "nok"},
+ },
+ }
+ physicalDriveTemperatureChartTmpl = module.Chart{
+ ID: "pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_temperature",
+ Title: "Physical Drive temperature",
+ Units: "Celsius",
+ Fam: "physical drives",
+ Ctx: "hpssa.physical_drive_temperature",
+ Type: module.Line,
+ Priority: prioPhysicalDriveTemperature,
+ Dims: module.Dims{
+ {ID: "pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_temperature", Name: "temperature"},
+ },
+ }
+)
+
+func (h *Hpssa) updateCharts(controllers map[string]*hpssaController) {
+ seenControllers := make(map[string]bool)
+ seenArrays := make(map[string]bool)
+ seenLDrives := make(map[string]bool)
+ seenPDrives := make(map[string]bool)
+
+ for _, cntrl := range controllers {
+ key := cntrl.uniqueKey()
+ seenControllers[key] = true
+ if _, ok := h.seenControllers[key]; !ok {
+ h.seenControllers[key] = cntrl
+ h.addControllerCharts(cntrl)
+ }
+
+ for _, pd := range cntrl.unassignedDrives {
+ key := pd.uniqueKey()
+ seenPDrives[key] = true
+ if _, ok := h.seenPDrives[key]; !ok {
+ h.seenPDrives[key] = pd
+ h.addPhysicalDriveCharts(pd)
+ }
+ }
+
+ for _, arr := range cntrl.arrays {
+ key := arr.uniqueKey()
+ seenArrays[key] = true
+ if _, ok := h.seenArrays[key]; !ok {
+ h.seenArrays[key] = arr
+ h.addArrayCharts(arr)
+ }
+
+ for _, ld := range arr.logicalDrives {
+ key := ld.uniqueKey()
+ seenLDrives[key] = true
+ if _, ok := h.seenLDrives[key]; !ok {
+ h.seenLDrives[key] = ld
+ h.addLogicalDriveCharts(ld)
+ }
+
+ for _, pd := range ld.physicalDrives {
+ key := pd.uniqueKey()
+ seenPDrives[key] = true
+ if _, ok := h.seenPDrives[key]; !ok {
+ h.seenPDrives[key] = pd
+ h.addPhysicalDriveCharts(pd)
+ }
+ }
+ }
+ }
+ }
+
+ for k, cntrl := range h.seenControllers {
+ if !seenControllers[k] {
+ delete(h.seenControllers, k)
+ h.removeControllerCharts(cntrl)
+ }
+ }
+ for k, arr := range h.seenArrays {
+ if !seenArrays[k] {
+ delete(h.seenArrays, k)
+ h.removeArrayCharts(arr)
+ }
+ }
+ for k, ld := range h.seenLDrives {
+ if !seenLDrives[k] {
+ delete(h.seenLDrives, k)
+ h.removeLogicalDriveCharts(ld)
+ }
+ }
+ for k, pd := range h.seenPDrives {
+ if !seenPDrives[k] {
+ delete(h.seenPDrives, k)
+ h.removePhysicalDriveCharts(pd)
+ }
+ }
+}
+
+func (h *Hpssa) addControllerCharts(cntrl *hpssaController) {
+ charts := controllerChartsTmpl.Copy()
+
+ if cntrl.controllerTemperatureC == "" {
+ _ = charts.Remove(controllerTemperatureChartTmpl.ID)
+ }
+
+ if cntrl.cacheBoardPresent != "True" {
+ _ = charts.Remove(controllerCacheModuleStatusChartTmpl.ID)
+ _ = charts.Remove(controllerCacheModuleTemperatureChartTmpl.ID)
+ _ = charts.Remove(controllerCacheModuleBatteryStatusChartTmpl.ID)
+ }
+ if cntrl.cacheModuleTemperatureC == "" {
+ _ = charts.Remove(controllerCacheModuleTemperatureChartTmpl.ID)
+ }
+ if cntrl.batteryCapacitorStatus == "" {
+ _ = charts.Remove(controllerCacheModuleBatteryStatusChartTmpl.ID)
+ }
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, strings.ToLower(cntrl.model), cntrl.slot)
+ chart.Labels = []module.Label{
+ {Key: "slot", Value: cntrl.slot},
+ {Key: "model", Value: cntrl.model},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, cntrl.model, cntrl.slot)
+ }
+ }
+
+ if err := h.Charts().Add(*charts...); err != nil {
+ h.Warning(err)
+ }
+}
+
+func (h *Hpssa) removeControllerCharts(cntrl *hpssaController) {
+ px := fmt.Sprintf("cntrl_%s_slot_%s_", strings.ToLower(cntrl.model), cntrl.slot)
+ h.removeCharts(px)
+}
+
+func (h *Hpssa) addArrayCharts(arr *hpssaArray) {
+ charts := arrayChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, arr.id, strings.ToLower(arr.cntrl.model), arr.cntrl.slot)
+ chart.Labels = []module.Label{
+ {Key: "slot", Value: arr.cntrl.slot},
+ {Key: "array_id", Value: arr.id},
+ {Key: "interface_type", Value: arr.interfaceType},
+ {Key: "array_type", Value: arr.arrayType},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, arr.id, arr.cntrl.model, arr.cntrl.slot)
+ }
+ }
+
+ if err := h.Charts().Add(*charts...); err != nil {
+ h.Warning(err)
+ }
+}
+
+func (h *Hpssa) removeArrayCharts(arr *hpssaArray) {
+ px := fmt.Sprintf("array_%s_cntrl_%s_slot_%s_", arr.id, strings.ToLower(arr.cntrl.model), arr.cntrl.slot)
+ h.removeCharts(px)
+}
+
+func (h *Hpssa) addLogicalDriveCharts(ld *hpssaLogicalDrive) {
+ charts := logicalDriveChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, ld.id, ld.arr.id, strings.ToLower(ld.cntrl.model), ld.cntrl.slot)
+ chart.Labels = []module.Label{
+ {Key: "slot", Value: ld.cntrl.slot},
+ {Key: "array_id", Value: ld.arr.id},
+ {Key: "logical_drive_id", Value: ld.id},
+ {Key: "disk_name", Value: ld.diskName},
+ {Key: "drive_type", Value: ld.driveType},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, ld.id, ld.arr.id, ld.cntrl.model, ld.cntrl.slot)
+ }
+ }
+
+ if err := h.Charts().Add(*charts...); err != nil {
+ h.Warning(err)
+ }
+}
+
+func (h *Hpssa) removeLogicalDriveCharts(ld *hpssaLogicalDrive) {
+ px := fmt.Sprintf("ld_%s_array_%s_cntrl_%s_slot_%s_", ld.id, ld.arr.id, strings.ToLower(ld.cntrl.model), ld.cntrl.slot)
+ h.removeCharts(px)
+}
+
+func (h *Hpssa) addPhysicalDriveCharts(pd *hpssaPhysicalDrive) {
+ charts := physicalDriveChartsTmpl.Copy()
+
+ if pd.currentTemperatureC == "" {
+ _ = charts.Remove(physicalDriveTemperatureChartTmpl.ID)
+ }
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, pd.location, pd.ldId(), pd.arrId(), strings.ToLower(pd.cntrl.model), pd.cntrl.slot)
+ chart.Labels = []module.Label{
+ {Key: "slot", Value: pd.cntrl.slot},
+ {Key: "array_id", Value: pd.arrId()},
+ {Key: "logical_drive_id", Value: pd.ldId()},
+ {Key: "location", Value: pd.location},
+ {Key: "interface_type", Value: pd.interfaceType},
+ {Key: "drive_type", Value: pd.driveType},
+ {Key: "model", Value: pd.model},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, pd.location, pd.ldId(), pd.arrId(), pd.cntrl.model, pd.cntrl.slot)
+ }
+ }
+
+ if err := h.Charts().Add(*charts...); err != nil {
+ h.Warning(err)
+ }
+}
+
+func (h *Hpssa) removePhysicalDriveCharts(pd *hpssaPhysicalDrive) {
+ px := fmt.Sprintf("pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_",
+ pd.location, pd.ldId(), pd.arrId(), strings.ToLower(pd.cntrl.model), pd.cntrl.slot)
+ h.removeCharts(px)
+}
+
+func (h *Hpssa) removeCharts(prefix string) {
+ for _, chart := range *h.Charts() {
+ if strings.HasPrefix(chart.ID, prefix) {
+ chart.MarkRemove()
+ chart.MarkNotCreated()
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/collect.go b/src/go/collectors/go.d.plugin/modules/hpssa/collect.go
new file mode 100644
index 0000000000..a0ce7d0bc2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/collect.go
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package hpssa
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+func (h *Hpssa) collect() (map[string]int64, error) {
+ data, err := h.exec.controllersInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ controllers, err := parseSsacliControllersInfo(data)
+ if err != nil {
+ return nil, err
+ }
+
+ mx := make(map[string]int64)
+
+ h.collectControllers(mx, controllers)
+ h.updateCharts(controllers)
+
+ return mx, nil
+}
+
+func (h *Hpssa) collectControllers(mx map[string]int64, controllers map[string]*hpssaController) {
+ for _, cntrl := range controllers {
+ h.collectController(mx, cntrl)
+
+ for _, pd := range cntrl.unassignedDrives {
+ h.collectPhysicalDrive(mx, pd)
+ }
+
+ for _, arr := range cntrl.arrays {
+ h.collectArray(mx, arr)
+
+ for _, ld := range arr.logicalDrives {
+ h.collectLogicalDrive(mx, ld)
+
+ for _, pd := range ld.physicalDrives {
+ h.collectPhysicalDrive(mx, pd)
+ }
+ }
+ }
+ }
+}
+
+func (h *Hpssa) collectController(mx map[string]int64, cntrl *hpssaController) {
+ px := fmt.Sprintf("cntrl_%s_slot_%s_", cntrl.model, cntrl.slot)
+
+ writeStatusOkNok(mx, px, cntrl.controllerStatus)
+
+ if v, ok := parseNumber(cntrl.controllerTemperatureC); ok {
+ mx[px+"temperature"] = v
+ }
+
+ mx[px+"cache_presence_status_present"] = 0
+ mx[px+"cache_presence_status_not_present"] = 0
+ if cntrl.cacheBoardPresent != "True" {
+ mx[px+"cache_presence_status_not_present"] = 1
+ return
+ }
+
+ mx[px+"cache_presence_status_present"] = 1
+
+ writeStatusOkNok(mx, px+"cache_", cntrl.cacheStatus)
+
+ if v, ok := parseNumber(cntrl.cacheModuleTemperatureC); ok {
+ mx[px+"cache_temperature"] = v
+ }
+
+ writeStatusOkNok(mx, px+"cache_battery_", cntrl.batteryCapacitorStatus)
+}
+
+func (h *Hpssa) collectArray(mx map[string]int64, arr *hpssaArray) {
+ if arr.cntrl == nil {
+ return
+ }
+
+ px := fmt.Sprintf("array_%s_cntrl_%s_slot_%s_",
+ arr.id, arr.cntrl.model, arr.cntrl.slot)
+
+ writeStatusOkNok(mx, px, arr.status)
+}
+
+func (h *Hpssa) collectLogicalDrive(mx map[string]int64, ld *hpssaLogicalDrive) {
+ if ld.cntrl == nil || ld.arr == nil {
+ return
+ }
+
+ px := fmt.Sprintf("ld_%s_array_%s_cntrl_%s_slot_%s_",
+ ld.id, ld.arr.id, ld.cntrl.model, ld.cntrl.slot)
+
+ writeStatusOkNok(mx, px, ld.status)
+}
+
+func (h *Hpssa) collectPhysicalDrive(mx map[string]int64, pd *hpssaPhysicalDrive) {
+ if pd.cntrl == nil {
+ return
+ }
+
+ px := fmt.Sprintf("pd_%s_ld_%s_array_%s_cntrl_%s_slot_%s_",
+ pd.location, pd.ldId(), pd.arrId(), pd.cntrl.model, pd.cntrl.slot)
+
+ writeStatusOkNok(mx, px, pd.status)
+
+ if v, ok := parseNumber(pd.currentTemperatureC); ok {
+ mx[px+"temperature"] = v
+ }
+}
+
+func parseNumber(s string) (int64, bool) {
+ v, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ return 0, false
+ }
+ return int64(v), true
+}
+
+func writeStatusOkNok(mx map[string]int64, prefix, status string) {
+ if !strings.HasSuffix(prefix, "_") {
+ prefix += "_"
+ }
+
+ mx[prefix+"status_ok"] = 0
+ mx[prefix+"status_nok"] = 0
+
+ switch status {
+ case "":
+ case "OK":
+ mx[prefix+"status_ok"] = 1
+ default:
+ mx[prefix+"status_nok"] = 1
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/config_schema.json b/src/go/collectors/go.d.plugin/modules/hpssa/config_schema.json
new file mode 100644
index 0000000000..788d7685e1
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/config_schema.json
@@ -0,0 +1,35 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "HPSSA collector configuration.",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 10
+ },
+ "timeout": {
+ "title": "Timeout",
+ "description": "Timeout for executing the `ssacli` binary, specified in seconds.",
+ "type": "number",
+ "minimum": 0.5,
+ "default": 2
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "timeout": {
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/exec.go b/src/go/collectors/go.d.plugin/modules/hpssa/exec.go
new file mode 100644
index 0000000000..e8bf511d72
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/exec.go
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package hpssa
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+)
+
+func newSsacliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *ssacliExec {
+ return &ssacliExec{
+ Logger: log,
+ ndsudoPath: ndsudoPath,
+ timeout: timeout,
+ }
+}
+
+type ssacliExec struct {
+ *logger.Logger
+
+ ndsudoPath string
+ timeout time.Duration
+}
+
+func (e *ssacliExec) controllersInfo() ([]byte, error) {
+ return e.execute("ssacli-controllers-info")
+}
+
+func (e *ssacliExec) execute(args ...string) ([]byte, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
+ e.Debugf("executing '%s'", cmd)
+
+ bs, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
+ }
+
+ return bs, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/hpssa.go b/src/go/collectors/go.d.plugin/modules/hpssa/hpssa.go
new file mode 100644
index 0000000000..1775ab38b5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/hpssa.go
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package hpssa
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("hpssa", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 10,
+ },
+ Create: func() module.Module { return New() },
+ })
+}
+
+func New() *Hpssa {
+ return &Hpssa{
+ Config: Config{
+ Timeout: web.Duration(time.Second * 2),
+ },
+ charts: &module.Charts{},
+ seenControllers: make(map[string]*hpssaController),
+ seenArrays: make(map[string]*hpssaArray),
+ seenLDrives: make(map[string]*hpssaLogicalDrive),
+ seenPDrives: make(map[string]*hpssaPhysicalDrive),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
+}
+
+type (
+ Hpssa struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ exec ssacli
+
+ seenControllers map[string]*hpssaController
+ seenArrays map[string]*hpssaArray
+ seenLDrives map[string]*hpssaLogicalDrive
+ seenPDrives map[string]*hpssaPhysicalDrive
+ }
+ ssacli interface {
+ controllersInfo() ([]byte, error)
+ }
+)
+
+func (h *Hpssa) Configuration() any {
+ return h.Config
+}
+
+func (h *Hpssa) Init() error {
+ ssacliExec, err := h.initSsacliExec()
+ if err != nil {
+ h.Errorf("ssacli exec initialization: %v", err)
+ return err
+ }
+ h.exec = ssacliExec
+
+ return nil
+}
+
+func (h *Hpssa) Check() error {
+ mx, err := h.collect()
+ if err != nil {
+ h.Error(err)
+ return err
+ }
+
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+
+ return nil
+}
+
+func (h *Hpssa) Charts() *module.Charts {
+ return h.charts
+}
+
+func (h *Hpssa) Collect() map[string]int64 {
+ mx, err := h.collect()
+ if err != nil {
+ h.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (h *Hpssa) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/hpssa/hpssa_test.go b/src/go/collectors/go.d.plugin/modules/hpssa/hpssa_test.go
new file mode 100644
index 0000000000..ed31165001
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/hpssa/hpssa_test.go
@@ -0,0 +1,430 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package hpssa
+
+import (
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+
+ dataP212andP410i, _ = os.ReadFile("testdata/ssacli-P212_P410i.txt")
+ dataP400ar, _ = os.ReadFile("testdata/ssacli-P400ar.txt")
+ dataP400iUnassigned, _ = os.ReadFile("testdata/ssacli-P400i-unassigned.txt")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+
+ "dataP212andP410i": dataP212andP410i,
+ "dataP400ar": dataP400ar,
+ "dataP400iUnassigned": dataP400iUnassigned,
+ } {
+ require.NotNil(t, data, name)
+ }
+}
+
+func TestHpssa_Init(t *testing.T) {
+ tests := map[string]struct {
+ config Config
+ wantFail bool
+ }{
+ "fails if 'ndsudo' not found": {
+ wantFail: true,
+ config: New().Config,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ hpe := New()
+
+ if test.wantFail {
+ assert.Error(t, hpe.Init())
+ } else {
+ assert.NoError(t, hpe.Init())
+ }
+ })
+ }
+}
+
+func TestHpssa_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *Hpssa
+ }{
+ "not initialized exec": {
+ prepare: func() *Hpssa {
+ return New()
+ },
+ },
+ "after check": {
+ prepare: func() *Hpssa {
+ hpe := New()
+ hpe.exec = prepareMockOkP212andP410i()
+ _ = hpe.Check()
+ return hpe
+ },
+ },
+ "after collect": {
+ prepare: func() *Hpssa {
+ hpe := New()
+ hpe.exec = prepareMockOkP212andP410i()
+ _ = hpe.Collect()
+ return hpe
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ hpe := test.prepare()
+
+ assert.NotPanics(t, hpe.Cleanup)
+ })
+ }
+}
+
+func TestHpssa_Charts(t *testing.T) {
+ assert.NotNil(t, New().Charts())
+}
+
+func TestHpssa_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockSsacliExec
+ wantFail bool
+ }{
+ "success P212 and P410i": {
+ wantFail: false,
+ prepareMock: prepareMockOkP212andP410i,
+ },
+ "success P400ar": {
+ wantFail: false,
+ prepareMock: prepareMockOkP400ar,
+ },
+ "success P400i with Unassigned": {
+ wantFail: false,
+ prepareMock: prepareMockOkP400iUnassigned,
+ },
+ "fails if error on controllersInfo()": {
+ wantFail: true,
+ prepareMock: prepareMockErr,
+ },
+ "fails if empty response": {
+ wantFail: true,
+ prepareMock: prepareMockEmptyResponse,
+ },
+ "fails if unexpected response": {
+ wantFail: true,
+ prepareMock: prepareMockUnexpectedResponse,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ hpe := New()
+ mock := test.prepareMock()
+ hpe.exec = mock
+
+ if test.wantFail {
+ assert.Error(t, hpe.Check())
+ } else {
+ assert.NoError(t, hpe.Check())
+ }
+ })
+ }
+}
+
+func TestHpssa_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockSsacliExec
+ wantMetrics map[string]int64
+ wantCharts int
+ }{
+ "success P212 and P410i": {
+ prepareMock: prepareMockOkP212andP410i,
+ wantCharts: (len(controllerChartsTmpl)*2 - 6) +
+ len(arrayChartsTmpl)*3 +
+ len(logicalDriveChartsTmpl)*3 +
+ len(physicalDriveChartsTmpl)*18,
+ wantMetrics: ma