summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIlya Mashchenko <ilya@netdata.cloud>2024-04-17 15:23:56 +0300
committerGitHub <noreply@github.com>2024-04-17 15:23:56 +0300
commitb8760c60eb862c2c5af1429bfba494d131d409b3 (patch)
treee9e80672728cc38cac870e24733aa8586932cb24
parent40ba94b036570b4cf0c17bc13d6b4b2482672eb1 (diff)
go.d rewrite python.d/adaptec_raid (#17428)
-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/adaptec_raid.conf5
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec.go107
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec_test.go281
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/charts.go129
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/collect.go28
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/collect_ld.go100
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/collect_pd.go128
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/config_schema.json35
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/exec.go50
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/init.go23
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/metadata.yaml138
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/config.json4
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/config.yaml2
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/getconfig-ld-current.txt30
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/getconfig-ld-old.txt33
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/getconfig-pd-current.txt216
-rw-r--r--src/go/collectors/go.d.plugin/modules/adaptecraid/testdata/getconfig-pd-old.txt107
-rw-r--r--src/go/collectors/go.d.plugin/modules/init.go1
-rw-r--r--src/health/health.d/adaptec_raid.conf29
21 files changed, 1448 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/README.md b/src/go/collectors/go.d.plugin/README.md
index 97341cb46b..6dc519bee1 100644
--- a/src/go/collectors/go.d.plugin/README.md
+++ b/src/go/collectors/go.d.plugin/README.md
@@ -50,6 +50,7 @@ see the appropriate collector readme.
| Name | Monitors |
|:------------------------------------------------------------------------------------------------------------------------------|:-----------------------------:|
+| [adaptec_raid](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/adaptecraid) | Adaptec Hardware RAID |
| [activemq](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/activemq) | ActiveMQ |
| [apache](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/apache) | Apache |
| [bind](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/bind) | ISC Bind |
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 29e1bd5663..86fa940650 100644
--- a/src/go/collectors/go.d.plugin/config/go.d.conf
+++ b/src/go/collectors/go.d.plugin/config/go.d.conf
@@ -15,6 +15,7 @@ max_procs: 0
# If you want to change any value, you need to uncomment out it first.
# IMPORTANT: Do not remove all spaces, just remove # symbol. There should be a space before module name.
modules:
+# adaptec_raid: yes
# activemq: yes
# apache: yes
# bind: yes
diff --git a/src/go/collectors/go.d.plugin/config/go.d/adaptec_raid.conf b/src/go/collectors/go.d.plugin/config/go.d/adaptec_raid.conf
new file mode 100644
index 0000000000..21c548f2d0
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/config/go.d/adaptec_raid.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/adaptecraid#readme
+
+jobs:
+ - name: adaptec_raid
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec.go
new file mode 100644
index 0000000000..a04cd6747f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+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("adaptec_raid", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 10,
+ },
+ Create: func() module.Module { return New() },
+ })
+}
+
+func New() *AdaptecRaid {
+ return &AdaptecRaid{
+ Config: Config{
+ Timeout: web.Duration(time.Second * 2),
+ },
+ charts: &module.Charts{},
+ lds: make(map[string]bool),
+ pds: make(map[string]bool),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
+}
+
+type (
+ AdaptecRaid struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ exec arcconfCli
+
+ lds map[string]bool
+ pds map[string]bool
+ }
+ arcconfCli interface {
+ logicalDevicesInfo() ([]byte, error)
+ physicalDevicesInfo() ([]byte, error)
+ }
+)
+
+func (a *AdaptecRaid) Configuration() any {
+ return a.Config
+}
+
+func (a *AdaptecRaid) Init() error {
+ arcconfExec, err := a.initArcconfCliExec()
+ if err != nil {
+ a.Errorf("arcconf exec initialization: %v", err)
+ return err
+ }
+ a.exec = arcconfExec
+
+ return nil
+}
+
+func (a *AdaptecRaid) Check() error {
+ mx, err := a.collect()
+ if err != nil {
+ a.Error(err)
+ return err
+ }
+
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+
+ return nil
+}
+
+func (a *AdaptecRaid) Charts() *module.Charts {
+ return a.charts
+}
+
+func (a *AdaptecRaid) Collect() map[string]int64 {
+ mx, err := a.collect()
+ if err != nil {
+ a.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (a *AdaptecRaid) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec_test.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec_test.go
new file mode 100644
index 0000000000..b93ec51af1
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/adaptec_test.go
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+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")
+
+ dataLogicalDevicesOld, _ = os.ReadFile("testdata/getconfig-ld-old.txt")
+ dataPhysicalDevicesOld, _ = os.ReadFile("testdata/getconfig-pd-old.txt")
+ dataLogicalDevicesCurrent, _ = os.ReadFile("testdata/getconfig-ld-current.txt")
+ dataPhysicalDevicesCurrent, _ = os.ReadFile("testdata/getconfig-pd-current.txt")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+
+ "dataLogicalDevicesOld": dataLogicalDevicesOld,
+ "dataPhysicalDevicesOld": dataPhysicalDevicesOld,
+ "dataLogicalDevicesCurrent": dataLogicalDevicesCurrent,
+ "dataPhysicalDevicesCurrent": dataPhysicalDevicesCurrent,
+ } {
+ require.NotNil(t, data, name)
+ }
+}
+
+func TestAdaptecRaid_ConfigurationSerialize(t *testing.T) {
+ module.TestConfigurationSerialize(t, &AdaptecRaid{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestAdaptecRaid_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) {
+ adaptec := New()
+
+ if test.wantFail {
+ assert.Error(t, adaptec.Init())
+ } else {
+ assert.NoError(t, adaptec.Init())
+ }
+ })
+ }
+}
+
+func TestAdaptecRaid_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *AdaptecRaid
+ }{
+ "not initialized exec": {
+ prepare: func() *AdaptecRaid {
+ return New()
+ },
+ },
+ "after check": {
+ prepare: func() *AdaptecRaid {
+ adaptec := New()
+ adaptec.exec = prepareMockOkCurrent()
+ _ = adaptec.Check()
+ return adaptec
+ },
+ },
+ "after collect": {
+ prepare: func() *AdaptecRaid {
+ adaptec := New()
+ adaptec.exec = prepareMockOkCurrent()
+ _ = adaptec.Collect()
+ return adaptec
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ adaptec := test.prepare()
+
+ assert.NotPanics(t, adaptec.Cleanup)
+ })
+ }
+}
+
+func TestAdaptecRaid_Charts(t *testing.T) {
+ assert.NotNil(t, New().Charts())
+}
+
+func TestAdaptecRaid_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockArcconfExec
+ wantFail bool
+ }{
+ "success case old data": {
+ wantFail: false,
+ prepareMock: prepareMockOkOld,
+ },
+ "success case current data": {
+ wantFail: false,
+ prepareMock: prepareMockOkCurrent,
+ },
+ "err on exec": {
+ wantFail: true,
+ prepareMock: prepareMockErr,
+ },
+ "unexpected response": {
+ wantFail: true,
+ prepareMock: prepareMockUnexpectedResponse,
+ },
+ "empty response": {
+ wantFail: true,
+ prepareMock: prepareMockEmptyResponse,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ adaptec := New()
+ mock := test.prepareMock()
+ adaptec.exec = mock
+
+ if test.wantFail {
+ assert.Error(t, adaptec.Check())
+ } else {
+ assert.NoError(t, adaptec.Check())
+ }
+ })
+ }
+}
+
+func TestAdaptecRaid_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockArcconfExec
+ wantMetrics map[string]int64
+ wantCharts int
+ }{
+ "success case old data": {
+ prepareMock: prepareMockOkOld,
+ wantCharts: len(ldChartsTmpl)*1 + (len(pdChartsTmpl)-1)*4,
+ wantMetrics: map[string]int64{
+ "ld_0_health_state_critical": 0,
+ "ld_0_health_state_ok": 1,
+ "pd_0_health_state_critical": 0,
+ "pd_0_health_state_ok": 1,
+ "pd_0_smart_warnings": 0,
+ "pd_1_health_state_critical": 0,
+ "pd_1_health_state_ok": 1,
+ "pd_1_smart_warnings": 0,
+ "pd_2_health_state_critical": 0,
+ "pd_2_health_state_ok": 1,
+ "pd_2_smart_warnings": 0,
+ "pd_3_health_state_critical": 0,
+ "pd_3_health_state_ok": 1,
+ "pd_3_smart_warnings": 0,
+ },
+ },
+ "success case current data": {
+ prepareMock: prepareMockOkCurrent,
+ wantCharts: len(ldChartsTmpl)*1 + (len(pdChartsTmpl)-1)*6,
+ wantMetrics: map[string]int64{
+ "ld_0_health_state_critical": 0,
+ "ld_0_health_state_ok": 1,
+ "pd_0_health_state_critical": 0,
+ "pd_0_health_state_ok": 1,
+ "pd_0_smart_warnings": 0,
+ "pd_1_health_state_critical": 0,
+ "pd_1_health_state_ok": 1,
+ "pd_1_smart_warnings": 0,
+ "pd_2_health_state_critical": 0,
+ "pd_2_health_state_ok": 1,
+ "pd_2_smart_warnings": 0,
+ "pd_3_health_state_critical": 0,
+ "pd_3_health_state_ok": 1,
+ "pd_3_smart_warnings": 0,
+ "pd_4_health_state_critical": 0,
+ "pd_4_health_state_ok": 1,
+ "pd_4_smart_warnings": 0,
+ "pd_5_health_state_critical": 0,
+ "pd_5_health_state_ok": 1,
+ "pd_5_smart_warnings": 0,
+ },
+ },
+ "err on exec": {
+ prepareMock: prepareMockErr,
+ },
+ "unexpected response": {
+ prepareMock: prepareMockUnexpectedResponse,
+ },
+ "empty response": {
+ prepareMock: prepareMockUnexpectedResponse,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ adaptec := New()
+ mock := test.prepareMock()
+ adaptec.exec = mock
+
+ mx := adaptec.Collect()
+
+ assert.Equal(t, test.wantMetrics, mx)
+ assert.Len(t, *adaptec.Charts(), test.wantCharts)
+ })
+ }
+}
+
+func prepareMockOkOld() *mockArcconfExec {
+ return &mockArcconfExec{
+ ldData: dataLogicalDevicesOld,
+ pdData: dataPhysicalDevicesOld,
+ }
+}
+
+func prepareMockOkCurrent() *mockArcconfExec {
+ return &mockArcconfExec{
+ ldData: dataLogicalDevicesCurrent,
+ pdData: dataPhysicalDevicesCurrent,
+ }
+}
+
+func prepareMockErr() *mockArcconfExec {
+ return &mockArcconfExec{
+ errOnInfo: true,
+ }
+}
+
+func prepareMockUnexpectedResponse() *mockArcconfExec {
+ resp := []byte(`
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
+`)
+ return &mockArcconfExec{
+ ldData: resp,
+ pdData: resp,
+ }
+}
+
+func prepareMockEmptyResponse() *mockArcconfExec {
+ return &mockArcconfExec{}
+}
+
+type mockArcconfExec struct {
+ errOnInfo bool
+ ldData []byte
+ pdData []byte
+}
+
+func (m *mockArcconfExec) logicalDevicesInfo() ([]byte, error) {
+ if m.errOnInfo {
+ return nil, errors.New("mock.logicalDevicesInfo() error")
+ }
+ return m.ldData, nil
+}
+
+func (m *mockArcconfExec) physicalDevicesInfo() ([]byte, error) {
+ if m.errOnInfo {
+ return nil, errors.New("mock.physicalDevicesInfo() error")
+ }
+ return m.pdData, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/charts.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/charts.go
new file mode 100644
index 0000000000..2a6c993306
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/charts.go
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioLDStatus = module.Priority + iota
+
+ prioPDState
+ prioPDSmartWarnings
+ prioPDSmartTemperature
+)
+
+var ldChartsTmpl = module.Charts{
+ ldStatusChartTmpl.Copy(),
+}
+
+var (
+ ldStatusChartTmpl = module.Chart{
+ ID: "logical_device_%s_status",
+ Title: "Logical Device status",
+ Units: "status",
+ Fam: "ld health",
+ Ctx: "adaptecraid.logical_device_status",
+ Type: module.Line,
+ Priority: prioLDStatus,
+ Dims: module.Dims{
+ {ID: "ld_%s_health_state_ok", Name: "ok"},
+ {ID: "ld_%s_health_state_critical", Name: "critical"},
+ },
+ }
+)
+
+var pdChartsTmpl = module.Charts{
+ pdStateChartTmpl.Copy(),
+ pdSmartWarningChartTmpl.Copy(),
+ pdTemperatureChartTmpl.Copy(),
+}
+
+var (
+ pdStateChartTmpl = module.Chart{
+ ID: "physical_device_%s_state",
+ Title: "Physical Device state",
+ Units: "state",
+ Fam: "pd health",
+ Ctx: "adaptecraid.physical_device_state",
+ Type: module.Line,
+ Priority: prioPDState,
+ Dims: module.Dims{
+ {ID: "pd_%s_health_state_ok", Name: "ok"},
+ {ID: "pd_%s_health_state_critical", Name: "critical"},
+ },
+ }
+ pdSmartWarningChartTmpl = module.Chart{
+ ID: "physical_device_%s_smart_warnings",
+ Title: "Physical Device SMART warnings",
+ Units: "warnings",
+ Fam: "pd smart",
+ Ctx: "adaptecraid.physical_device_smart_warnings",
+ Type: module.Line,
+ Priority: prioPDSmartWarnings,
+ Dims: module.Dims{
+ {ID: "pd_%s_smart_warnings", Name: "smart"},
+ },
+ }
+ pdTemperatureChartTmpl = module.Chart{
+ ID: "physical_device_%s_temperature",
+ Title: "Physical Device temperature",
+ Units: "Celsius",
+ Fam: "pd temperature",
+ Ctx: "adaptecraid.physical_device_temperature",
+ Type: module.Line,
+ Priority: prioPDSmartTemperature,
+ Dims: module.Dims{
+ {ID: "pd_%s_temperature", Name: "temperature"},
+ },
+ }
+)
+
+func (a *AdaptecRaid) addLogicalDeviceCharts(ld *logicalDevice) {
+ charts := ldChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, ld.number)
+ chart.Labels = []module.Label{
+ {Key: "ld_number", Value: ld.number},
+ {Key: "ld_name", Value: ld.name},
+ {Key: "raid_level", Value: ld.raidLevel},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, ld.number)
+ }
+ }
+
+ if err := a.Charts().Add(*charts...); err != nil {
+ a.Warning(err)
+ }
+}
+
+func (a *AdaptecRaid) addPhysicalDeviceCharts(pd *physicalDevice) {
+ charts := pdChartsTmpl.Copy()
+
+ if _, err := strconv.ParseInt(pd.temperature, 10, 64); err != nil {
+ _ = charts.Remove(pdTemperatureChartTmpl.ID)
+ }
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, pd.number)
+ chart.Labels = []module.Label{
+ {Key: "pd_number", Value: pd.number},
+ {Key: "location", Value: pd.location},
+ {Key: "vendor", Value: pd.vendor},
+ {Key: "model", Value: pd.model},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, pd.number)
+ }
+ }
+
+ if err := a.Charts().Add(*charts...); err != nil {
+ a.Warning(err)
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/collect.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect.go
new file mode 100644
index 0000000000..b4439ba8e0
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "strings"
+)
+
+func (a *AdaptecRaid) collect() (map[string]int64, error) {
+ mx := make(map[string]int64)
+
+ if err := a.collectLogicalDevices(mx); err != nil {
+ return nil, err
+ }
+ if err := a.collectPhysicalDevices(mx); err != nil {
+ return nil, err
+ }
+
+ return mx, nil
+}
+
+func getColonSepValue(line string) string {
+ i := strings.IndexByte(line, ':')
+ if i == -1 {
+ return ""
+ }
+ return strings.TrimSpace(line[i+1:])
+}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_ld.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_ld.go
new file mode 100644
index 0000000000..180f974903
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_ld.go
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+type logicalDevice struct {
+ number string
+ name string
+ raidLevel string
+ status string
+ failedStripes string
+}
+
+func (a *AdaptecRaid) collectLogicalDevices(mx map[string]int64) error {
+ bs, err := a.exec.logicalDevicesInfo()
+ if err != nil {
+ return err
+ }
+
+ devices, err := parseLogicDevInfo(bs)
+ if err != nil {
+ return err
+ }
+
+ if len(devices) == 0 {
+ return errors.New("no logical devices found")
+ }
+
+ for _, ld := range devices {
+ if !a.lds[ld.number] {
+ a.lds[ld.number] = true
+ a.addLogicalDeviceCharts(ld)
+ }
+
+ px := fmt.Sprintf("ld_%s_", ld.number)
+
+ // Unfortunately, all available states are unknown.
+ mx[px+"health_state_ok"] = 0
+ mx[px+"health_state_critical"] = 0
+ if isOkLDStatus(ld) {
+ mx[px+"health_state_ok"] = 1
+ } else {
+ mx[px+"health_state_critical"] = 1
+ }
+ }
+
+ return nil
+}
+
+func isOkLDStatus(ld *logicalDevice) bool {
+ // https://github.com/thomas-krenn/check_adaptec_raid/blob/a104fd88deede87df4f07403b44394bffb30c5c3/check_adaptec_raid#L340
+ return ld.status == "Optimal"
+}
+
+func parseLogicDevInfo(bs []byte) (map[string]*logicalDevice, error) {
+ devices := make(map[string]*logicalDevice)
+
+ var ld *logicalDevice
+
+ sc := bufio.NewScanner(bytes.NewReader(bs))
+
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+
+ if strings.HasPrefix(line, "Logical device number") ||
+ strings.HasPrefix(line, "Logical Device number") {
+ parts := strings.Fields(line)
+ num := parts[len(parts)-1]
+ ld = &logicalDevice{number: num}
+ devices[num] = ld
+ continue
+ }
+
+ if ld == nil {
+ continue
+ }
+
+ switch {
+ case strings.HasPrefix(line, "Logical device name"),
+ strings.HasPrefix(line, "Logical Device name"):
+ ld.name = getColonSepValue(line)
+ case strings.HasPrefix(line, "RAID level"):
+ ld.raidLevel = getColonSepValue(line)
+ case strings.HasPrefix(line, "Status of logical device"),
+ strings.HasPrefix(line, "Status of Logical Device"):
+ ld.status = getColonSepValue(line)
+ case strings.HasPrefix(line, "Failed stripes"):
+ ld.failedStripes = getColonSepValue(line)
+ }
+ }
+
+ return devices, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_pd.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_pd.go
new file mode 100644
index 0000000000..272266b47f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/collect_pd.go
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+type physicalDevice struct {
+ number string
+ state string
+ location string
+ vendor string
+ model string
+ smart string
+ smartWarnings string
+ powerState string
+ temperature string
+}
+
+func (a *AdaptecRaid) collectPhysicalDevices(mx map[string]int64) error {
+ bs, err := a.exec.physicalDevicesInfo()
+ if err != nil {
+ return err
+ }
+
+ devices, err := parsePhysDevInfo(bs)
+ if err != nil {
+ return err
+ }
+
+ if len(devices) == 0 {
+ return errors.New("no physical devices found")
+ }
+
+ for _, pd := range devices {
+ if !a.pds[pd.number] {
+ a.pds[pd.number] = true
+ a.addPhysicalDeviceCharts(pd)
+ }
+
+ px := fmt.Sprintf("pd_%s_", pd.number)
+
+ // Unfortunately, all available states are unknown.
+ mx[px+"health_state_ok"] = 0
+ mx[px+"health_state_critical"] = 0
+ if isOkPDState(pd) {
+ mx[px+"health_state_ok"] = 1
+ } else {
+ mx[px+"health_state_critical"] = 1
+ }
+
+ if v, err := strconv.ParseInt(pd.smartWarnings, 10, 64); err == nil {
+ mx[px+"smart_warnings"] = v
+ }
+ if v, err := strconv.ParseInt(pd.temperature, 10, 64); err == nil {
+ mx[px+"temperature"] = v
+ }
+ }
+
+ return nil
+}
+
+func isOkPDState(pd *physicalDevice) bool {
+ // https://github.com/thomas-krenn/check_adaptec_raid/blob/a104fd88deede87df4f07403b44394bffb30c5c3/check_adaptec_raid#L455
+ switch pd.state {
+ case "Online",
+ "Global Hot-Spare",
+ "Dedicated Hot-Spare",
+ "Pooled Hot-Spare",
+ "Hot Spare",
+ "Ready",
+ "Online (JBOD)",
+ "Raw (Pass Through)":
+ return true
+ }
+ return false
+}
+
+func parsePhysDevInfo(bs []byte) (map[string]*physicalDevice, error) {
+ devices := make(map[string]*physicalDevice)
+
+ var pd *physicalDevice
+
+ sc := bufio.NewScanner(bytes.NewReader(bs))
+
+ for sc.Scan() {
+ line := strings.TrimSpace(sc.Text())
+
+ if strings.HasPrefix(line, "Device #") {
+ num := strings.TrimPrefix(line, "Device #")
+ pd = &physicalDevice{number: num}
+ devices[num] = pd
+ continue
+ }
+
+ if pd == nil {
+ continue
+ }
+
+ switch {
+ case strings.HasPrefix(line, "State"):
+ pd.state = getColonSepValue(line)
+ case strings.HasPrefix(line, "Reported Location"):
+ pd.location = getColonSepValue(line)
+ case strings.HasPrefix(line, "Vendor"):
+ pd.vendor = getColonSepValue(line)
+ case strings.HasPrefix(line, "Model"):
+ pd.model = getColonSepValue(line)
+ case strings.HasPrefix(line, "S.M.A.R.T. warnings"):
+ pd.smartWarnings = getColonSepValue(line)
+ case strings.HasPrefix(line, "S.M.A.R.T."):
+ pd.smart = getColonSepValue(line)
+ case strings.HasPrefix(line, "Power State"):
+ pd.powerState = getColonSepValue(line)
+ case strings.HasPrefix(line, "Temperature"):
+ v := getColonSepValue(line) // '42 C/ 107 F' or 'Not Supported'
+ pd.temperature = strings.Fields(v)[0]
+ }
+ }
+
+ return devices, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/adaptecraid/config_schema.json b/src/go/collectors/go.d.plugin/modules/adaptecraid/config_schema.json
new file mode 100644
index 0000000000..ad54f15854
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/config_schema.json
@@ -0,0 +1,35 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Adaptec RAID 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 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/adaptecraid/exec.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/exec.go
new file mode 100644
index 0000000000..3a34840cf0
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/exec.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "context"
+ "fmt"
+ "os/exec"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+)
+
+func newArcconfCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *arcconfCliExec {
+ return &arcconfCliExec{
+ Logger: log,
+ ndsudoPath: ndsudoPath,
+ timeout: timeout,
+ }
+}
+
+type arcconfCliExec struct {
+ *logger.Logger
+
+ ndsudoPath string
+ timeout time.Duration
+}
+
+func (e *arcconfCliExec) logicalDevicesInfo() ([]byte, error) {
+ return e.execute("arcconf-ld-info")
+}
+
+func (e *arcconfCliExec) physicalDevicesInfo() ([]byte, error) {
+ return e.execute("arcconf-pd-info")
+}
+
+func (e *arcconfCliExec) 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/adaptecraid/init.go b/src/go/collectors/go.d.plugin/modules/adaptecraid/init.go
new file mode 100644
index 0000000000..fe26f7bff3
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/adaptecraid/init.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package adaptecraid
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/executable"
+)
+
+func (a *AdaptecRaid) initArcconfCliExec() (arcconfCli, error) {
+ ndsudoPath := filepath.Join(executable.Directory