diff options
author | Ilya Mashchenko <ilya@netdata.cloud> | 2024-05-14 10:59:25 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-14 10:59:25 +0300 |
commit | e8618460d1ac4c26592b983f2c88628aea0d82b1 (patch) | |
tree | 826deb1f89b41d3c5d9784e29c9f2603c098a171 /src | |
parent | 519467f4ece870b516772f0fef5af8661ef118f6 (diff) |
go.d hpssa (#17637)
Diffstat (limited to 'src')
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 |