@cryptotaxi247 / netdata-1 / commits / 686fdb86f

feat(collectors): add Windows support for hardware monitoring collectors (#21635)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: ilyam8 <ilya@netdata.cloud>

Costa Tsaousis committed Feb 13, 2026 at 04:21 UTC 686fdb86f912b5cedf2ad3780b6363b6cee67c5c
37 files changed +555 -113
src/go/plugin/go.d/collector/adaptecraid/charts.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/collect.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/collect_ld.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/collect_pd.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/collector.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/collector_test.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
src/go/plugin/go.d/collector/adaptecraid/exec.go
+33 -10
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 import (
@@ -16,22 +14,47 @@ type arcconfCli interface {
14 physicalDevicesInfo() ([]byte, error)
15 }
16
19 -func newArcconfCliExec(timeout time.Duration, log *logger.Logger) *arcconfCliExec {
20 - return &arcconfCliExec{
17 +// ndsudoArcconfCliExec executes arcconf via ndsudo (Linux/BSD)
18 +type ndsudoArcconfCliExec struct {
19 + *logger.Logger
20 + timeout time.Duration
21 +}
22 +
23 +func newNdsudoArcconfCliExec(timeout time.Duration, log *logger.Logger) *ndsudoArcconfCliExec {
24 + return &ndsudoArcconfCliExec{
25 Logger: log,
26 timeout: timeout,
27 }
28 }
29
26 -type arcconfCliExec struct {
30 +func (e *ndsudoArcconfCliExec) logicalDevicesInfo() ([]byte, error) {
31 + return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-ld-info")
32 +}
33 +
34 +func (e *ndsudoArcconfCliExec) physicalDevicesInfo() ([]byte, error) {
35 + return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-pd-info")
36 +}
37 +
38 +// directArcconfCliExec executes arcconf directly (Windows)
39 +type directArcconfCliExec struct {
40 *logger.Logger
28 - timeout time.Duration
41 +
42 + arcconfPath string
43 + timeout time.Duration
44 }
45
31 -func (e *arcconfCliExec) logicalDevicesInfo() ([]byte, error) {
32 - return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-ld-info")
46 +func newDirectArcconfCliExec(arcconfPath string, timeout time.Duration, log *logger.Logger) *directArcconfCliExec {
47 + return &directArcconfCliExec{
48 + Logger: log,
49 + arcconfPath: arcconfPath,
50 + timeout: timeout,
51 + }
52 }
53
35 -func (e *arcconfCliExec) physicalDevicesInfo() ([]byte, error) {
36 - return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-pd-info")
54 +func (e *directArcconfCliExec) logicalDevicesInfo() ([]byte, error) {
55 + return ndexec.RunDirect(e.Logger, e.timeout, e.arcconfPath, "GETCONFIG", "1", "LD")
56 +}
57 +
58 +func (e *directArcconfCliExec) physicalDevicesInfo() ([]byte, error) {
59 + return ndexec.RunDirect(e.Logger, e.timeout, e.arcconfPath, "GETCONFIG", "1", "PD")
60 }
src/go/plugin/go.d/collector/adaptecraid/init.go
+35 -3
@@ -1,10 +1,42 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package adaptecraid
4
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 +)
13 +
14 func (c *Collector) initArcconfCliExec() (arcconfCli, error) {
8 - arcconfExec := newArcconfCliExec(c.Timeout.Duration(), c.Logger)
15 + if runtime.GOOS == "windows" {
16 + return c.initDirectArcconfCliExec()
17 + }
18 + return c.initNdsudoArcconfCliExec()
19 +}
20 +
21 +func (c *Collector) initNdsudoArcconfCliExec() (arcconfCli, error) {
22 + arcconfExec := newNdsudoArcconfCliExec(c.Timeout.Duration(), c.Logger)
23 return arcconfExec, nil
24 }
25 +
26 +func (c *Collector) initDirectArcconfCliExec() (arcconfCli, error) {
27 + path, err := ndexec.FindBinary(
28 + []string{"arcconf", "ARCCONF"},
29 + []string{
30 + filepath.Join(os.Getenv("ProgramFiles"), "Adaptec", "ARCCONF", "arcconf.exe"),
31 + filepath.Join(os.Getenv("ProgramFiles"), "Microsemi", "ARCCONF", "arcconf.exe"),
32 + filepath.Join(os.Getenv("ProgramFiles(x86)"), "Adaptec", "ARCCONF", "arcconf.exe"),
33 + },
34 + )
35 + if err != nil {
36 + return nil, fmt.Errorf("arcconf: %w", err)
37 + }
38 +
39 + c.Debugf("found arcconf at: %s", path)
40 +
41 + return newDirectArcconfCliExec(path, c.Timeout.Duration(), c.Logger), nil
42 +}
src/go/plugin/go.d/collector/hpssa/exec.go
+27 -6
@@ -13,19 +13,40 @@ type ssacliBinary interface {
13 controllersInfo() ([]byte, error)
14 }
15
16 -func newSsacliExec(timeout time.Duration, log *logger.Logger) *ssacliExec {
17 - return &ssacliExec{
16 +// ndsudoSsacliExec executes ssacli via ndsudo (Linux/BSD)
17 +type ndsudoSsacliExec struct {
18 + *logger.Logger
19 +
20 + timeout time.Duration
21 +}
22 +
23 +func newNdsudoSsacliExec(timeout time.Duration, log *logger.Logger) *ndsudoSsacliExec {
24 + return &ndsudoSsacliExec{
25 Logger: log,
26 timeout: timeout,
27 }
28 }
29
23 -type ssacliExec struct {
30 +func (e *ndsudoSsacliExec) controllersInfo() ([]byte, error) {
31 + return ndexec.RunNDSudo(e.Logger, e.timeout, "ssacli-controllers-info")
32 +}
33 +
34 +// directSsacliExec executes ssacli directly (Windows)
35 +type directSsacliExec struct {
36 *logger.Logger
37
26 - timeout time.Duration
38 + ssacliPath string
39 + timeout time.Duration
40 }
41
29 -func (e *ssacliExec) controllersInfo() ([]byte, error) {
30 - return ndexec.RunNDSudo(e.Logger, e.timeout, "ssacli-controllers-info")
42 +func newDirectSsacliExec(ssacliPath string, timeout time.Duration, log *logger.Logger) *directSsacliExec {
43 + return &directSsacliExec{
44 + Logger: log,
45 + ssacliPath: ssacliPath,
46 + timeout: timeout,
47 + }
48 +}
49 +
50 +func (e *directSsacliExec) controllersInfo() ([]byte, error) {
51 + return ndexec.RunDirect(e.Logger, e.timeout, e.ssacliPath, "ctrl", "all", "show", "config", "detail")
52 }
src/go/plugin/go.d/collector/hpssa/init.go
+35 -1
@@ -2,8 +2,42 @@
2
3 package hpssa
4
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 +)
13 +
14 func (c *Collector) initSsacliBinary() (ssacliBinary, error) {
6 - ssacliExec := newSsacliExec(c.Timeout.Duration(), c.Logger)
15 + if runtime.GOOS == "windows" {
16 + return c.initDirectSsacliExec()
17 + }
18 + return c.initNdsudoSsacliExec()
19 +}
20
21 +func (c *Collector) initNdsudoSsacliExec() (ssacliBinary, error) {
22 + ssacliExec := newNdsudoSsacliExec(c.Timeout.Duration(), c.Logger)
23 return ssacliExec, nil
24 }
25 +
26 +func (c *Collector) initDirectSsacliExec() (ssacliBinary, error) {
27 + path, err := ndexec.FindBinary(
28 + []string{"ssacli", "SSACLI", "hpssacli"},
29 + []string{
30 + filepath.Join(os.Getenv("ProgramFiles"), "Smart Storage Administrator", "ssacli", "bin", "ssacli.exe"),
31 + filepath.Join(os.Getenv("ProgramFiles"), "HP", "hpssacli", "bin", "hpssacli.exe"),
32 + filepath.Join(os.Getenv("ProgramFiles"), "Compaq", "Hpacucli", "Bin", "hpacucli.exe"),
33 + filepath.Join(os.Getenv("ProgramFiles(x86)"), "Smart Storage Administrator", "ssacli", "bin", "ssacli.exe"),
34 + },
35 + )
36 + if err != nil {
37 + return nil, fmt.Errorf("ssacli: %w", err)
38 + }
39 +
40 + c.Debugf("found ssacli at: %s", path)
41 +
42 + return newDirectSsacliExec(path, c.Timeout.Duration(), c.Logger), nil
43 +}
src/go/plugin/go.d/collector/megacli/charts.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/collect.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/collect_bbu.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/collect_phys_drives.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/collector.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/collector_test.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
src/go/plugin/go.d/collector/megacli/exec.go
+33 -10
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 import (
@@ -16,23 +14,48 @@ type megaCli interface {
14 bbuInfo() ([]byte, error)
15 }
16
19 -func newMegaCliExec(timeout time.Duration, log *logger.Logger) *megaCliExec {
20 - return &megaCliExec{
17 +// ndsudoMegaCliExec executes megacli via ndsudo (Linux/BSD)
18 +type ndsudoMegaCliExec struct {
19 + *logger.Logger
20 +
21 + timeout time.Duration
22 +}
23 +
24 +func newNdsudoMegaCliExec(timeout time.Duration, log *logger.Logger) *ndsudoMegaCliExec {
25 + return &ndsudoMegaCliExec{
26 Logger: log,
27 timeout: timeout,
28 }
29 }
30
26 -type megaCliExec struct {
31 +func (e *ndsudoMegaCliExec) physDrivesInfo() ([]byte, error) {
32 + return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-disk-info")
33 +}
34 +
35 +func (e *ndsudoMegaCliExec) bbuInfo() ([]byte, error) {
36 + return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-battery-info")
37 +}
38 +
39 +// directMegaCliExec executes megacli directly (Windows)
40 +type directMegaCliExec struct {
41 *logger.Logger
42
29 - timeout time.Duration
43 + megacliPath string
44 + timeout time.Duration
45 }
46
32 -func (e *megaCliExec) physDrivesInfo() ([]byte, error) {
33 - return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-disk-info")
47 +func newDirectMegaCliExec(megacliPath string, timeout time.Duration, log *logger.Logger) *directMegaCliExec {
48 + return &directMegaCliExec{
49 + Logger: log,
50 + megacliPath: megacliPath,
51 + timeout: timeout,
52 + }
53 }
54
36 -func (e *megaCliExec) bbuInfo() ([]byte, error) {
37 - return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-battery-info")
55 +func (e *directMegaCliExec) physDrivesInfo() ([]byte, error) {
56 + return ndexec.RunDirect(e.Logger, e.timeout, e.megacliPath, "-LDPDInfo", "-aAll", "-NoLog")
57 +}
58 +
59 +func (e *directMegaCliExec) bbuInfo() ([]byte, error) {
60 + return ndexec.RunDirect(e.Logger, e.timeout, e.megacliPath, "-AdpBbuCmd", "-aAll", "-NoLog")
61 }
src/go/plugin/go.d/collector/megacli/init.go
+34 -3
@@ -1,11 +1,42 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package megacli
4
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 +)
13 +
14 func (c *Collector) initMegaCliExec() (megaCli, error) {
8 - megaExec := newMegaCliExec(c.Timeout.Duration(), c.Logger)
15 + if runtime.GOOS == "windows" {
16 + return c.initDirectMegaCliExec()
17 + }
18 + return c.initNdsudoMegaCliExec()
19 +}
20
21 +func (c *Collector) initNdsudoMegaCliExec() (megaCli, error) {
22 + megaExec := newNdsudoMegaCliExec(c.Timeout.Duration(), c.Logger)
23 return megaExec, nil
24 }
25 +
26 +func (c *Collector) initDirectMegaCliExec() (megaCli, error) {
27 + path, err := ndexec.FindBinary(
28 + []string{"megacli", "MegaCli", "MegaCli64", "megacli64"},
29 + []string{
30 + filepath.Join(os.Getenv("ProgramFiles"), "LSI", "MegaCLI", "MegaCli64.exe"),
31 + filepath.Join(os.Getenv("ProgramFiles"), "Broadcom", "MegaCLI", "MegaCli64.exe"),
32 + filepath.Join(os.Getenv("ProgramFiles(x86)"), "MegaCLI", "MegaCli64.exe"),
33 + },
34 + )
35 + if err != nil {
36 + return nil, fmt.Errorf("megacli: %w", err)
37 + }
38 +
39 + c.Debugf("found megacli at: %s", path)
40 +
41 + return newDirectMegaCliExec(path, c.Timeout.Duration(), c.Logger), nil
42 +}
src/go/plugin/go.d/collector/nvidia_smi/exec.go
+23
@@ -8,6 +8,7 @@ import (
8 "errors"
9 "os/exec"
10 "path/filepath"
11 + "runtime"
12 "strconv"
13 "sync"
14 "time"
@@ -24,6 +25,13 @@ type nvidiaSmiBinary interface {
25
26 func newNvidiaSmiBinary(path string, cfg Config, log *logger.Logger) (nvidiaSmiBinary, error) {
27 if !cfg.LoopMode {
28 + if runtime.GOOS == "windows" {
29 + return &nvidiaSmiDirectExec{
30 + Logger: log,
31 + binPath: path,
32 + timeout: cfg.Timeout.Duration(),
33 + }, nil
34 + }
35 return &nvidiaSmiExec{
36 Logger: log,
37 binPath: path,
@@ -45,6 +53,7 @@ func newNvidiaSmiBinary(path string, cfg Config, log *logger.Logger) (nvidiaSmiB
53 return smi, nil
54 }
55
56 +// nvidiaSmiExec executes nvidia-smi via nd-run (Linux/BSD)
57 type nvidiaSmiExec struct {
58 *logger.Logger
59
@@ -58,6 +67,20 @@ func (e *nvidiaSmiExec) queryGPUInfo() ([]byte, error) {
67
68 func (e *nvidiaSmiExec) stop() error { return nil }
69
70 +// nvidiaSmiDirectExec executes nvidia-smi directly (Windows)
71 +type nvidiaSmiDirectExec struct {
72 + *logger.Logger
73 +
74 + binPath string
75 + timeout time.Duration
76 +}
77 +
78 +func (e *nvidiaSmiDirectExec) queryGPUInfo() ([]byte, error) {
79 + return ndexec.RunDirect(e.Logger, e.timeout, e.binPath, "-q", "-x")
80 +}
81 +
82 +func (e *nvidiaSmiDirectExec) stop() error { return nil }
83 +
84 type nvidiaSmiLoopExec struct {
85 *logger.Logger
86
src/go/plugin/go.d/collector/nvidia_smi/init.go
+21 -2
@@ -6,16 +6,30 @@ import (
6 "fmt"
7 "os"
8 "os/exec"
9 + "path/filepath"
10 + "runtime"
11
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pathvalidate"
14 )
15
16 func (c *Collector) initNvidiaSmiExec() (nvidiaSmiBinary, error) {
17 binPath := c.BinaryPath
15 - if _, err := os.Stat(binPath); os.IsNotExist(err) {
18 + if binPath == "" || !fileExists(binPath) {
19 path, err := exec.LookPath(c.binName)
20 if err != nil {
18 - return nil, fmt.Errorf("error on lookup '%s': %v", c.binName, err)
21 + if runtime.GOOS == "windows" {
22 + path, err = ndexec.FindBinary(
23 + nil,
24 + []string{
25 + filepath.Join(os.Getenv("ProgramFiles"), "NVIDIA Corporation", "NVSMI", "nvidia-smi.exe"),
26 + filepath.Join(os.Getenv("SystemRoot"), "System32", "nvidia-smi.exe"),
27 + },
28 + )
29 + }
30 + if err != nil {
31 + return nil, fmt.Errorf("error on lookup '%s': %v", c.binName, err)
32 + }
33 }
34 binPath = path
35 }
@@ -27,3 +41,8 @@ func (c *Collector) initNvidiaSmiExec() (nvidiaSmiBinary, error) {
41
42 return newNvidiaSmiBinary(validatedPath, c.Config, c.Logger)
43 }
44 +
45 +func fileExists(path string) bool {
46 + _, err := os.Stat(path)
47 + return !os.IsNotExist(err)
48 +}
src/go/plugin/go.d/collector/nvme/charts.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 import (
src/go/plugin/go.d/collector/nvme/collect.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 import (
src/go/plugin/go.d/collector/nvme/collector.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 import (
src/go/plugin/go.d/collector/nvme/collector_test.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 import (
src/go/plugin/go.d/collector/nvme/exec.go
+41 -5
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 import (
@@ -11,6 +9,7 @@ import (
9 "strings"
10 "time"
11
12 + "github.com/netdata/netdata/go/plugins/logger"
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
14 )
15
@@ -143,11 +142,12 @@ type nvmeCli interface {
142 smartLog(devicePath string) (*nvmeDeviceSmartLog, error)
143 }
144
146 -type nvmeCLIExec struct {
145 +// ndsudoNvmeCliExec executes nvme via ndsudo (Linux/BSD)
146 +type ndsudoNvmeCliExec struct {
147 timeout time.Duration
148 }
149
150 -func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
150 +func (n *ndsudoNvmeCliExec) list() (*nvmeDeviceList, error) {
151 bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-list")
152 if err != nil {
153 return nil, err
@@ -161,7 +161,7 @@ func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
161 return &v, nil
162 }
163
164 -func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
164 +func (n *ndsudoNvmeCliExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
165 bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-smart-log", "--device", devicePath)
166 if err != nil {
167 return nil, err
@@ -174,3 +174,39 @@ func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
174
175 return &v, nil
176 }
177 +
178 +// directNvmeCliExec executes nvme directly (Windows)
179 +type directNvmeCliExec struct {
180 + *logger.Logger
181 +
182 + nvmePath string
183 + timeout time.Duration
184 +}
185 +
186 +func (n *directNvmeCliExec) list() (*nvmeDeviceList, error) {
187 + bs, err := ndexec.RunDirect(n.Logger, n.timeout, n.nvmePath, "list", "--output-format=json")
188 + if err != nil {
189 + return nil, err
190 + }
191 +
192 + var v nvmeDeviceList
193 + if err := json.Unmarshal(bs, &v); err != nil {
194 + return nil, err
195 + }
196 +
197 + return &v, nil
198 +}
199 +
200 +func (n *directNvmeCliExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
201 + bs, err := ndexec.RunDirect(n.Logger, n.timeout, n.nvmePath, "smart-log", devicePath, "--output-format=json")
202 + if err != nil {
203 + return nil, err
204 + }
205 +
206 + var v nvmeDeviceSmartLog
207 + if err := json.Unmarshal(bs, &v); err != nil {
208 + return nil, err
209 + }
210 +
211 + return &v, nil
212 +}
src/go/plugin/go.d/collector/nvme/init.go
+37 -3
@@ -1,11 +1,45 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package nvme
4
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 +)
13 +
14 func (c *Collector) initNVMeCLIExec() (nvmeCli, error) {
8 - nvmeExec := &nvmeCLIExec{timeout: c.Timeout.Duration()}
15 + if runtime.GOOS == "windows" {
16 + return c.initDirectNvmeCliExec()
17 + }
18 + return c.initNdsudoNvmeCliExec()
19 +}
20
21 +func (c *Collector) initNdsudoNvmeCliExec() (nvmeCli, error) {
22 + nvmeExec := &ndsudoNvmeCliExec{timeout: c.Timeout.Duration()}
23 return nvmeExec, nil
24 }
25 +
26 +func (c *Collector) initDirectNvmeCliExec() (nvmeCli, error) {
27 + path, err := ndexec.FindBinary(
28 + []string{"nvme", "nvme-cli"},
29 + []string{
30 + filepath.Join(os.Getenv("ProgramFiles"), "nvme-cli", "nvme.exe"),
31 + filepath.Join(os.Getenv("ProgramFiles(x86)"), "nvme-cli", "nvme.exe"),
32 + },
33 + )
34 + if err != nil {
35 + return nil, fmt.Errorf("nvme: %w", err)
36 + }
37 +
38 + c.Debugf("found nvme at: %s", path)
39 +
40 + return &directNvmeCliExec{
41 + Logger: c.Logger,
42 + nvmePath: path,
43 + timeout: c.Timeout.Duration(),
44 + }, nil
45 +}
src/go/plugin/go.d/collector/smartctl/init.go
+11 -13
@@ -5,11 +5,11 @@ package smartctl
5 import (
6 "fmt"
7 "os"
8 - "os/exec"
8 "path/filepath"
9 "runtime"
10
11 "github.com/netdata/netdata/go/plugins/pkg/matcher"
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13 )
14
15 func (c *Collector) validateConfig() error {
@@ -54,19 +54,17 @@ func (c *Collector) initNdsudoSmartctlCli() (smartctlCli, error) {
54 }
55
56 func (c *Collector) initDirectSmartctlCli() (smartctlCli, error) {
57 - smartctlPath, err := exec.LookPath("smartctl")
57 + path, err := ndexec.FindBinary(
58 + []string{"smartctl"},
59 + []string{
60 + filepath.Join(os.Getenv("ProgramFiles"), "smartmontools", "bin", "smartctl.exe"),
61 + },
62 + )
63 if err != nil {
59 - if runtime.GOOS != "windows" {
60 - return nil, fmt.Errorf("smartctl executable not found in PATH: %v", err)
61 - }
62 - defaultWinPath := filepath.Join("C:\\Program Files\\smartmontools\\bin", "smartctl.exe")
63 - if _, err := os.Stat(defaultWinPath); err != nil {
64 - return nil, fmt.Errorf("smartctl executable not found in PATH or default location: %v", err)
65 - }
66 - smartctlPath = defaultWinPath
64 + return nil, fmt.Errorf("smartctl: %w", err)
65 }
66
69 - c.Debugf("found smartctl at: %s", smartctlPath)
70 - smartctlExec := newDirectSmartctlCli(smartctlPath, c.Timeout.Duration(), c.Logger)
71 - return smartctlExec, nil
67 + c.Debugf("found smartctl at: %s", path)
68 +
69 + return newDirectSmartctlCli(path, c.Timeout.Duration(), c.Logger), nil
70 }
src/go/plugin/go.d/collector/storcli/charts.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
src/go/plugin/go.d/collector/storcli/collect.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import "fmt"
src/go/plugin/go.d/collector/storcli/collect_controllers.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
src/go/plugin/go.d/collector/storcli/collect_drives.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
src/go/plugin/go.d/collector/storcli/collector.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
src/go/plugin/go.d/collector/storcli/collector_test.go
-2
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
src/go/plugin/go.d/collector/storcli/exec.go
+33 -10
@@ -1,7 +1,5 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 import (
@@ -16,23 +14,48 @@ type storCli interface {
14 drivesInfo() ([]byte, error)
15 }
16
19 -func newStorCliExec(timeout time.Duration, log *logger.Logger) *storCliExec {
20 - return &storCliExec{
17 +// ndsudoStorCliExec executes storcli via ndsudo (Linux/BSD)
18 +type ndsudoStorCliExec struct {
19 + *logger.Logger
20 +
21 + timeout time.Duration
22 +}
23 +
24 +func newNdsudoStorCliExec(timeout time.Duration, log *logger.Logger) *ndsudoStorCliExec {
25 + return &ndsudoStorCliExec{
26 Logger: log,
27 timeout: timeout,
28 }
29 }
30
26 -type storCliExec struct {
31 +func (e *ndsudoStorCliExec) controllersInfo() ([]byte, error) {
32 + return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-controllers-info")
33 +}
34 +
35 +func (e *ndsudoStorCliExec) drivesInfo() ([]byte, error) {
36 + return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-drives-info")
37 +}
38 +
39 +// directStorCliExec executes storcli directly (Windows)
40 +type directStorCliExec struct {
41 *logger.Logger
42
29 - timeout time.Duration
43 + storcliPath string
44 + timeout time.Duration
45 }
46
32 -func (e *storCliExec) controllersInfo() ([]byte, error) {
33 - return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-controllers-info")
47 +func newDirectStorCliExec(storcliPath string, timeout time.Duration, log *logger.Logger) *directStorCliExec {
48 + return &directStorCliExec{
49 + Logger: log,
50 + storcliPath: storcliPath,
51 + timeout: timeout,
52 + }
53 }
54
36 -func (e *storCliExec) drivesInfo() ([]byte, error) {
37 - return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-drives-info")
55 +func (e *directStorCliExec) controllersInfo() ([]byte, error) {
56 + return ndexec.RunDirect(e.Logger, e.timeout, e.storcliPath, "/cALL", "show", "all", "J", "nolog")
57 +}
58 +
59 +func (e *directStorCliExec) drivesInfo() ([]byte, error) {
60 + return ndexec.RunDirect(e.Logger, e.timeout, e.storcliPath, "/cALL/eALL/sALL", "show", "all", "J", "nolog")
61 }
src/go/plugin/go.d/collector/storcli/init.go
+34 -3
@@ -1,11 +1,42 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -//go:build linux || freebsd || openbsd || netbsd || dragonfly
4 -
3 package storcli
4
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 +
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 +)
13 +
14 func (c *Collector) initStorCliExec() (storCli, error) {
8 - storExec := newStorCliExec(c.Timeout.Duration(), c.Logger)
15 + if runtime.GOOS == "windows" {
16 + return c.initDirectStorCliExec()
17 + }
18 + return c.initNdsudoStorCliExec()
19 +}
20
21 +func (c *Collector) initNdsudoStorCliExec() (storCli, error) {
22 + storExec := newNdsudoStorCliExec(c.Timeout.Duration(), c.Logger)
23 return storExec, nil
24 }
25 +
26 +func (c *Collector) initDirectStorCliExec() (storCli, error) {
27 + path, err := ndexec.FindBinary(
28 + []string{"storcli", "storcli64", "StorCLI", "StorCLI64"},
29 + []string{
30 + filepath.Join(os.Getenv("ProgramFiles"), "Broadcom", "StorCLI", "storcli64.exe"),
31 + filepath.Join(os.Getenv("ProgramFiles"), "LSI", "StorCLI", "storcli64.exe"),
32 + filepath.Join(os.Getenv("ProgramFiles(x86)"), "StorCLI", "storcli64.exe"),
33 + },
34 + )
35 + if err != nil {
36 + return nil, fmt.Errorf("storcli: %w", err)
37 + }
38 +
39 + c.Debugf("found storcli at: %s", path)
40 +
41 + return newDirectStorCliExec(path, c.Timeout.Duration(), c.Logger), nil
42 +}
src/go/plugin/go.d/pkg/ndexec/ndexec.go
+50
@@ -6,6 +6,7 @@ import (
6 "bytes"
7 "context"
8 "fmt"
9 + "os"
10 "os/exec"
11 "path/filepath"
12 "runtime"
@@ -114,6 +115,55 @@ func SetRunnerPathsForTests(ndRunPath, ndSudoPath string) {
115 }
116 }
117
118 +// RunDirect runs binPath directly with a timeout, without any wrapper (nd-run/ndsudo).
119 +// Returns stdout. On error, includes the command string and a trimmed stderr snippet.
120 +func RunDirect(log *logger.Logger, timeout time.Duration, binPath string, args ...string) ([]byte, error) {
121 + ctx, cancel := context.WithTimeout(context.Background(), timeout)
122 + defer cancel()
123 +
124 + cmd := exec.CommandContext(ctx, binPath, args...)
125 +
126 + if log != nil {
127 + log.Debugf("executing '%s'", cmd)
128 + }
129 +
130 + var stderr bytes.Buffer
131 + cmd.Stderr = &stderr
132 +
133 + bs, err := cmd.Output()
134 + if err != nil {
135 + s := stderr.String()
136 + if len(s) > stderrLimit {
137 + s = s[:stderrLimit] + "… (truncated)"
138 + }
139 + return nil, fmt.Errorf("'%s' execution failed: %w (stderr: %s)", cmd, err, strings.TrimSpace(s))
140 + }
141 +
142 + return bs, nil
143 +}
144 +
145 +// FindBinary searches for a binary by trying names in PATH first,
146 +// then checking defaultPaths on the filesystem.
147 +// Returns the first found path, or an error if not found.
148 +func FindBinary(names []string, defaultPaths []string) (string, error) {
149 + for _, name := range names {
150 + if path, err := exec.LookPath(name); err == nil {
151 + return path, nil
152 + }
153 + }
154 +
155 + for _, path := range defaultPaths {
156 + if fi, err := os.Stat(path); err == nil && !fi.IsDir() {
157 + return path, nil
158 + }
159 + }
160 +
161 + if len(names) == 0 {
162 + return "", fmt.Errorf("executable not found in default locations")
163 + }
164 + return "", fmt.Errorf("executable not found in PATH (%s) or default locations", strings.Join(names, ", "))
165 +}
166 +
167 func (r *runner) run(log *logger.Logger, timeout time.Duration, dir string, helperPath, label string, env []string, argv ...string) ([]byte, string, ResourceUsage, error) {
168 ctx, cancel := context.WithTimeout(context.Background(), timeout)
169 defer cancel()
src/go/plugin/go.d/pkg/ndexec/ndexec_test.go
+108
@@ -120,6 +120,114 @@ exec "$@"
120 }
121 }
122
123 +func TestRunDirect(t *testing.T) {
124 + if runtime.GOOS == "windows" {
125 + t.Skip("uses sh scripts")
126 + }
127 +
128 + tmp := t.TempDir()
129 +
130 + writeExe := func(path, body string) {
131 + require.NoError(t, os.WriteFile(path, []byte(body), 0o755))
132 + }
133 +
134 + echoArgs := filepath.Join(tmp, "echoargs.sh")
135 + writeExe(echoArgs, "#!/bin/sh\nprintf '%s|' \"$@\"\necho\n")
136 +
137 + stderrScript := filepath.Join(tmp, "stderr.sh")
138 + writeExe(stderrScript, "#!/bin/sh\necho 'some error' 1>&2\nexit 1\n")
139 +
140 + sleeper := filepath.Join(tmp, "sleep.sh")
141 + writeExe(sleeper, "#!/bin/sh\nsleep 2\n")
142 +
143 + t.Run("success", func(t *testing.T) {
144 + out, err := RunDirect(nil, time.Second, echoArgs, "hello", "world")
145 + require.NoError(t, err)
146 + assert.Equal(t, "hello|world|\n", string(out))
147 + })
148 +
149 + t.Run("non-zero exit with stderr", func(t *testing.T) {
150 + _, err := RunDirect(nil, time.Second, stderrScript)
151 + require.Error(t, err)
152 + assert.Contains(t, err.Error(), "execution failed")
153 + assert.Contains(t, err.Error(), "some error")
154 + })
155 +
156 + t.Run("timeout", func(t *testing.T) {
157 + _, err := RunDirect(nil, 200*time.Millisecond, sleeper)
158 + require.Error(t, err)
159 + assert.Contains(t, err.Error(), "execution failed")
160 + })
161 +
162 + t.Run("binary not found", func(t *testing.T) {
163 + _, err := RunDirect(nil, time.Second, filepath.Join(tmp, "nonexistent"))
164 + require.Error(t, err)
165 + assert.Contains(t, err.Error(), "execution failed")
166 + })
167 +
168 + t.Run("long stderr truncated", func(t *testing.T) {
169 + longStderr := filepath.Join(tmp, "longstderr.sh")
170 + writeExe(longStderr, "#!/bin/sh\nprintf '"+strings.Repeat("x", 9000)+"' 1>&2\nexit 1\n")
171 +
172 + _, err := RunDirect(nil, 5*time.Second, longStderr)
173 + require.Error(t, err)
174 + assert.Contains(t, err.Error(), "truncated")
175 + })
176 +}
177 +
178 +func TestFindBinary(t *testing.T) {
179 + tmp := t.TempDir()
180 +
181 + binPath := filepath.Join(tmp, "testbin")
182 + require.NoError(t, os.WriteFile(binPath, []byte("#!/bin/sh\n"), 0o755))
183 +
184 + t.Run("found in default paths", func(t *testing.T) {
185 + path, err := FindBinary(
186 + []string{"nonexistent-binary-12345"},
187 + []string{filepath.Join(tmp, "missing"), binPath},
188 + )
189 + require.NoError(t, err)
190 + assert.Equal(t, binPath, path)
191 + })
192 +
193 + t.Run("not found anywhere", func(t *testing.T) {
194 + _, err := FindBinary(
195 + []string{"nonexistent-binary-12345"},
196 + []string{filepath.Join(tmp, "also-missing")},
197 + )
198 + require.Error(t, err)
199 + assert.Contains(t, err.Error(), "executable not found")
200 + })
201 +
202 + t.Run("skips directories", func(t *testing.T) {
203 + dirPath := filepath.Join(tmp, "adir")
204 + require.NoError(t, os.Mkdir(dirPath, 0o755))
205 +
206 + _, err := FindBinary(
207 + []string{"nonexistent-binary-12345"},
208 + []string{dirPath},
209 + )
210 + require.Error(t, err)
211 + })
212 +
213 + t.Run("nil names searches only default paths", func(t *testing.T) {
214 + path, err := FindBinary(nil, []string{binPath})
215 + require.NoError(t, err)
216 + assert.Equal(t, binPath, path)
217 + })
218 +
219 + t.Run("nil names not found", func(t *testing.T) {
220 + _, err := FindBinary(nil, []string{filepath.Join(tmp, "missing")})
221 + require.Error(t, err)
222 + assert.Contains(t, err.Error(), "executable not found")
223 + })
224 +
225 + t.Run("both nil", func(t *testing.T) {
226 + _, err := FindBinary(nil, nil)
227 + require.Error(t, err)
228 + })
229 +}
230 +
231 func TestRunUnprivilegedWithOptionsCmdWorkingDir(t *testing.T) {
232 if runtime.GOOS == "windows" {
233 t.Skip("uses sh scripts")