diff options
author | Ilya Mashchenko <ilya@netdata.cloud> | 2024-04-17 15:23:56 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-17 15:23:56 +0300 |
commit | b8760c60eb862c2c5af1429bfba494d131d409b3 (patch) | |
tree | e9e80672728cc38cac870e24733aa8586932cb24 | |
parent | 40ba94b036570b4cf0c17bc13d6b4b2482672eb1 (diff) |
go.d rewrite python.d/adaptec_raid (#17428)
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 |